From 3e2459721186c145526334eb9a7061b1a579b09d Mon Sep 17 00:00:00 2001 From: carrypan Date: Tue, 22 Jun 2021 14:52:02 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feature:=20=E5=BC=80=E5=8F=91=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=8C=96=E7=BB=84=E4=BB=B6plugin-starter=20#61?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devops-plugin/build.gradle.kts | 1 + .../devops-plugin/plugin-api/build.gradle.kts | 1 + .../tencent/devops/plugin/api/Extension.kt | 9 + .../devops/plugin/api/ExtensionPoint.kt | 7 + .../devops/plugin/api/ExtensionRegistry.kt | 68 +++++++ .../devops/plugin/api/ExtensionType.kt | 16 ++ .../devops/plugin/api/PluginConstants.kt | 3 + .../tencent/devops/plugin/api/PluginInfo.kt | 27 +++ .../devops/plugin/api/PluginManager.kt | 35 ++++ .../plugin/api/PluginManagerExtensions.kt | 17 ++ .../devops/plugin/api/PluginMetadata.kt | 31 ++++ .../devops/plugin/api/PluginScanner.kt | 22 +++ .../plugin-core/build.gradle.kts | 8 + .../devops/plugin/PluginAutoConfiguration.kt | 35 ++++ .../devops/plugin/config/PluginProperties.kt | 11 ++ .../plugin/core/DefaultPluginManager.kt | 137 ++++++++++++++ .../plugin/core/DefaultPluginScanner.kt | 44 +++++ .../devops/plugin/core/PluginClassLoader.kt | 25 +++ .../devops/plugin/core/PluginLoader.kt | 121 ++++++++++++ .../devops/plugin/spring/PluginEndpoint.kt | 70 +++++++ .../spring/PluginEndpointAutoConfiguration.kt | 17 ++ .../plugin/spring/SpringExtensionRegistry.kt | 172 ++++++++++++++++++ .../main/resources/META-INF/spring.factories | 3 + .../plugin-processor/build.gradle.kts | 6 + .../processor/ExtensionAnnotationProcessor.kt | 86 +++++++++ .../devops/plugin/processor/ExtensionFiles.kt | 29 +++ .../javax.annotation.processing.Processor | 1 + .../build.gradle.kts | 5 + settings.gradle.kts | 24 ++- 29 files changed, 1018 insertions(+), 13 deletions(-) create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/build.gradle.kts create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-api/build.gradle.kts create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/Extension.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/ExtensionPoint.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/ExtensionRegistry.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/ExtensionType.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginConstants.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginInfo.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginManager.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginManagerExtensions.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginMetadata.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginScanner.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-core/build.gradle.kts create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/PluginAutoConfiguration.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/config/PluginProperties.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/core/DefaultPluginManager.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/core/DefaultPluginScanner.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/core/PluginClassLoader.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/core/PluginLoader.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/spring/PluginEndpoint.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/spring/PluginEndpointAutoConfiguration.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/spring/SpringExtensionRegistry.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/resources/META-INF/spring.factories create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-processor/build.gradle.kts create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-processor/src/main/kotlin/com/tencent/devops/plugin/processor/ExtensionAnnotationProcessor.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-processor/src/main/kotlin/com/tencent/devops/plugin/processor/ExtensionFiles.kt create mode 100644 devops-boot-project/devops-boot-core/devops-plugin/plugin-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor create mode 100644 devops-boot-project/devops-boot-starters/devops-boot-starter-plugin/build.gradle.kts diff --git a/devops-boot-project/devops-boot-core/devops-plugin/build.gradle.kts b/devops-boot-project/devops-boot-core/devops-plugin/build.gradle.kts new file mode 100644 index 0000000..5a2196f --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/build.gradle.kts @@ -0,0 +1 @@ +description = "DevOps Boot Plugin" diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/build.gradle.kts b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/build.gradle.kts new file mode 100644 index 0000000..a504d8a --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/build.gradle.kts @@ -0,0 +1 @@ +description = "DevOps Boot Plugin Api" diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/Extension.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/Extension.kt new file mode 100644 index 0000000..2d23bc7 --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/Extension.kt @@ -0,0 +1,9 @@ +package com.tencent.devops.plugin.api + +/** + * 用于标记扩展类 + */ +@Retention(AnnotationRetention.SOURCE) +@Target(AnnotationTarget.CLASS) +@MustBeDocumented +annotation class Extension diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/ExtensionPoint.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/ExtensionPoint.kt new file mode 100644 index 0000000..6a263a1 --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/ExtensionPoint.kt @@ -0,0 +1,7 @@ +package com.tencent.devops.plugin.api + +/** + * 扩展点 + * 插件中的扩展类需要实现该接口 + */ +interface ExtensionPoint diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/ExtensionRegistry.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/ExtensionRegistry.kt new file mode 100644 index 0000000..9d242ec --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/ExtensionRegistry.kt @@ -0,0 +1,68 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.tencent.devops.plugin.api + +/** + * 扩展注册器 + */ +interface ExtensionRegistry { + + /** + * 注册扩展controller + * @param plugin 插件名称 + * @param type 扩展controller class类型 + */ + fun registerExtensionController(plugin: String, name: String, type: Class<*>) + + /** + * 注册扩展点 + * @param plugin 插件名称 + * @param type 扩展point class类型 + */ + fun registerExtensionPoint(plugin: String, name: String, type: Class<*>) + + /** + * 查找扩展点 + * @param type 扩展点类型 + */ + fun findExtensionPoints(type: Class): List + + /** + * 注销插件[plugin]相关的扩展point + */ + fun unregisterExtensionPointsByPlugin(plugin: String) + + /** + * 注销插件[plugin]相关的扩展controller + */ + fun unregisterExtensionControllerByPlugin(plugin: String) +} diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/ExtensionType.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/ExtensionType.kt new file mode 100644 index 0000000..f0749ca --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/ExtensionType.kt @@ -0,0 +1,16 @@ +package com.tencent.devops.plugin.api + +/** + * 扩展类型 + */ +enum class ExtensionType(val identifier: String) { + /** + * 扩展controller,可以通过插件实现新增接口 + */ + CONTROLLER("ExtensionController"), + + /** + * 扩展点,可以通过插件实现新增逻辑 + */ + POINT("ExtensionPoint") +} diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginConstants.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginConstants.kt new file mode 100644 index 0000000..ad513b6 --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginConstants.kt @@ -0,0 +1,3 @@ +package com.tencent.devops.plugin.api + +const val EXTENSION_LOCATION = "META-INF/extensions.factories" diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginInfo.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginInfo.kt new file mode 100644 index 0000000..fb88763 --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginInfo.kt @@ -0,0 +1,27 @@ +package com.tencent.devops.plugin.api + +/** + * 插件信息 + */ +class PluginInfo( + /** + * 插件id + */ + val id: String, + /** + * 插件元数据信息 + */ + val metadata: PluginMetadata, + /** + * 文件摘要 + */ + val digest: String, + /** + * 插件扩展点 + */ + val extensionPoints: List, + /** + * 扩展controller + */ + val extensionControllers: List +) diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginManager.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginManager.kt new file mode 100644 index 0000000..abcf32d --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginManager.kt @@ -0,0 +1,35 @@ +package com.tencent.devops.plugin.api + +/** + * 插件管理器 + */ +interface PluginManager { + + /** + * 加载所有插件 + */ + fun load() + + /** + * 加载插件 + * @param id 插件id + */ + fun load(id: String) + + /** + * 卸载插件 + * @param id 插件id + */ + fun unload(id: String) + + /** + * 查找扩展点列表 + * @param clazz 扩展点类型 + */ + fun findExtensionPoints(clazz: Class): List + + /** + * 获取注册的插件列表 + */ + fun getPluginMap(): Map +} diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginManagerExtensions.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginManagerExtensions.kt new file mode 100644 index 0000000..ef69108 --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginManagerExtensions.kt @@ -0,0 +1,17 @@ +package com.tencent.devops.plugin.api + +/** + * 查找扩展点, kotlin风格扩展函数 + */ +inline fun PluginManager.find(): List { + return this.findExtensionPoints(T::class.java) +} + +/** + * 查找扩展点并遍历执行扩展逻辑 + */ +inline fun PluginManager.applyExtension(block: T.() -> Unit) { + this.findExtensionPoints(T::class.java).forEach { + block(it) + } +} diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginMetadata.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginMetadata.kt new file mode 100644 index 0000000..c739e34 --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginMetadata.kt @@ -0,0 +1,31 @@ +package com.tencent.devops.plugin.api + +/** + * 插件metadata信息 + */ +data class PluginMetadata( + /** + * 插件id,要求唯一 + */ + val id: String, + /** + * 插件名称,要求唯一,先保持和id一致 + */ + val name: String, + /** + * 插件版本,语义化版本格式 + */ + val version: String, + /** + * 插件生效范围 + */ + val scope: List, + /** + * 插件作者 + */ + val author: String? = null, + /** + * 插件描述 + */ + val description: String? = null +) diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginScanner.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginScanner.kt new file mode 100644 index 0000000..5693ebc --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-api/src/main/kotlin/com/tencent/devops/plugin/api/PluginScanner.kt @@ -0,0 +1,22 @@ +package com.tencent.devops.plugin.api + +import java.nio.file.Path + +/** + * 插件扫描器 + */ +interface PluginScanner { + + /** + * 扫描插件路径 + * @return 插件路径集合 + */ + fun scan(): List + + /** + * 扫描指定插件路径 + * 此方法要求插件必须按照指定格式命名:plugin-xxx-1.0.0.jar,其中xxx代表插件id + * @param id 插件id + */ + fun scan(id: String): Path? +} diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/build.gradle.kts b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/build.gradle.kts new file mode 100644 index 0000000..5323650 --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/build.gradle.kts @@ -0,0 +1,8 @@ +description = "DevOps Boot Plugin Core" + +dependencies { + api(project(":devops-boot-project:devops-boot-core:devops-plugin:plugin-api")) + api("org.springframework.boot:spring-boot-starter") + api("org.springframework:spring-webmvc") + compileOnly("org.springframework.boot:spring-boot-starter-actuator") +} diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/PluginAutoConfiguration.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/PluginAutoConfiguration.kt new file mode 100644 index 0000000..c6fb0eb --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/PluginAutoConfiguration.kt @@ -0,0 +1,35 @@ +package com.tencent.devops.plugin + +import com.tencent.devops.plugin.api.ExtensionRegistry +import com.tencent.devops.plugin.api.PluginManager +import com.tencent.devops.plugin.api.PluginScanner +import com.tencent.devops.plugin.config.PluginProperties +import com.tencent.devops.plugin.core.DefaultPluginManager +import com.tencent.devops.plugin.core.DefaultPluginScanner +import com.tencent.devops.plugin.spring.SpringExtensionRegistry +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(PluginProperties::class) +class PluginAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(PluginScanner::class) + fun pluginScanner(pluginProperties: PluginProperties) = DefaultPluginScanner(pluginProperties) + + @Bean + @ConditionalOnMissingBean(ExtensionRegistry::class) + fun extensionRegistry() = SpringExtensionRegistry() + + @Bean + @ConditionalOnMissingBean(PluginManager::class) + fun pluginManager( + pluginScanner: PluginScanner, + extensionRegistry: ExtensionRegistry + ): PluginManager { + return DefaultPluginManager(pluginScanner, extensionRegistry) + } +} diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/config/PluginProperties.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/config/PluginProperties.kt new file mode 100644 index 0000000..ca87844 --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/config/PluginProperties.kt @@ -0,0 +1,11 @@ +package com.tencent.devops.plugin.config + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("plugin") +data class PluginProperties( + /** + * 插件路径 + */ + var path: String = "plugins" +) diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/core/DefaultPluginManager.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/core/DefaultPluginManager.kt new file mode 100644 index 0000000..d6603e0 --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/core/DefaultPluginManager.kt @@ -0,0 +1,137 @@ +package com.tencent.devops.plugin.core + +import com.tencent.devops.plugin.api.ExtensionPoint +import com.tencent.devops.plugin.api.ExtensionRegistry +import com.tencent.devops.plugin.api.PluginInfo +import com.tencent.devops.plugin.api.PluginManager +import com.tencent.devops.plugin.api.PluginScanner +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.context.event.ApplicationReadyEvent +import org.springframework.context.event.EventListener + +/** + * 插件管理器 + */ +class DefaultPluginManager( + private val pluginScanner: PluginScanner, + private val extensionRegistry: ExtensionRegistry +) : PluginManager { + + private val pluginMap = mutableMapOf() + + @Value("\${service.name}") + private var applicationName: String? = null + + @EventListener(ApplicationReadyEvent::class) + @Synchronized + override fun load() { + try { + pluginScanner.scan().forEach { + val pluginLoader = PluginLoader(it) + val pluginInfo = pluginLoader.loadPlugin() + registerPluginIfNecessary(pluginInfo, pluginLoader.classLoader) + } + } catch (ignored: Exception) { + logger.error("Failed to load plugin: ${ignored.message}", ignored) + throw ignored + } + } + + @Synchronized + override fun load(id: String) { + try { + val path = pluginScanner.scan(id) + checkNotNull(path) { "Plugin[$id] jar file not found" } + val pluginLoader = PluginLoader(path) + val pluginInfo = pluginLoader.loadPlugin() + registerPluginIfNecessary(pluginInfo, pluginLoader.classLoader) + } catch (ignored: Exception) { + logger.error("Failed to load plugin[$id]: ${ignored.message}", ignored) + throw ignored + } + } + + @Synchronized + override fun unload(id: String) { + try { + if (!pluginMap.containsKey(id)) { + return + } + extensionRegistry.unregisterExtensionPointsByPlugin(id) + extensionRegistry.unregisterExtensionControllerByPlugin(id) + pluginMap.remove(id) + logger.info("Success unregister plugin[$id]") + } catch (ignored: Exception) { + logger.error("Failed to unload plugin[$id]: ${ignored.message}", ignored) + throw ignored + } + } + + override fun findExtensionPoints(clazz: Class): List { + return extensionRegistry.findExtensionPoints(clazz) + } + + override fun getPluginMap(): Map { + return pluginMap + } + + /** + * 注销插件[pluginInfo] + * 如果插件scope不符合,或者已经存在则不会注册 + */ + private fun registerPluginIfNecessary(pluginInfo: PluginInfo, classLoader: ClassLoader) { + if (!checkScope(pluginInfo)) { + logger.info("Plugin[${pluginInfo.id}] scope does not contain $applicationName, skip register") + return + } + if (checkExist(pluginInfo)) { + logger.info("Plugin[${pluginInfo.id}] has been loaded, skip register") + return + } + if (pluginMap.containsKey(pluginInfo.id)) { + // unregister loaded extension points + extensionRegistry.unregisterExtensionPointsByPlugin(pluginInfo.id) + // unregister loaded extension controller + extensionRegistry.unregisterExtensionControllerByPlugin(pluginInfo.id) + // remove + pluginMap.remove(pluginInfo.id) + } + + // register extension points + pluginInfo.extensionPoints.forEach { + val type = classLoader.loadClass(it) + val name = type.interfaces[0].name + extensionRegistry.registerExtensionPoint(pluginInfo.id, name, type) + } + // register extension controller + pluginInfo.extensionControllers.forEach { + val type = classLoader.loadClass(it) + extensionRegistry.registerExtensionController(pluginInfo.id, it, type) + } + // save + pluginMap[pluginInfo.id] = pluginInfo + logger.info("Success register plugin[${pluginInfo.id}]") + } + + /** + * 检查插件范围是否有效 + */ + private fun checkScope(pluginInfo: PluginInfo): Boolean { + if (pluginInfo.metadata.scope.isEmpty()) { + return true + } + return pluginInfo.metadata.scope.contains(applicationName) + } + + /** + * 检查插件是否已存在 + */ + private fun checkExist(pluginInfo: PluginInfo): Boolean { + return pluginMap[pluginInfo.id]?.digest == pluginInfo.digest + } + + companion object { + private val logger = LoggerFactory.getLogger(DefaultPluginManager::class.java) + } +} diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/core/DefaultPluginScanner.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/core/DefaultPluginScanner.kt new file mode 100644 index 0000000..6fbcbd3 --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/core/DefaultPluginScanner.kt @@ -0,0 +1,44 @@ +package com.tencent.devops.plugin.core + +import com.tencent.devops.plugin.api.PluginScanner +import com.tencent.devops.plugin.config.PluginProperties +import org.slf4j.LoggerFactory +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.streams.toList + +/** + * 插件扫描器默认实现 + * 扫描指定路径下jar文件 + */ +class DefaultPluginScanner( + private val pluginProperties: PluginProperties +) : PluginScanner { + + override fun scan(): List { + val pluginDir = Paths.get(pluginProperties.path) + if (!Files.isDirectory(pluginDir)) { + logger.error("Failed to load plugin, Path[$pluginDir] is not a directory.") + return emptyList() + } + return Files.walk(pluginDir).filter { it.toString().endsWith(".jar") }.toList() + } + + override fun scan(id: String): Path? { + val pluginDir = Paths.get(pluginProperties.path) + if (!Files.isDirectory(pluginDir)) { + logger.error("Failed to load plugin, Path[$pluginDir] is not a directory.") + return null + } + return Files.walk(pluginDir).filter { + val filename = it.toString() + val name = filename.substringAfter("-").substringBeforeLast("-") + name == id + }.toList().firstOrNull() + } + + companion object { + private val logger = LoggerFactory.getLogger(DefaultPluginScanner::class.java) + } +} diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/core/PluginClassLoader.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/core/PluginClassLoader.kt new file mode 100644 index 0000000..9ee56ae --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/core/PluginClassLoader.kt @@ -0,0 +1,25 @@ +package com.tencent.devops.plugin.core + +import java.net.URLClassLoader +import java.nio.file.Path + +/** + * 插件classloader + * 每个插件对应一个单独的classloader + */ +class PluginClassLoader( + pluginPath: Path, + parentLoader: ClassLoader +) : URLClassLoader(arrayOf(pluginPath.toUri().toURL()), parentLoader) { + override fun loadClass(name: String?): Class<*> { + return super.loadClass(name) + } + + override fun findClass(name: String?): Class<*> { + return super.findClass(name) + } + + override fun loadClass(name: String?, resolve: Boolean): Class<*> { + return super.loadClass(name, resolve) + } +} diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/core/PluginLoader.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/core/PluginLoader.kt new file mode 100644 index 0000000..c153a5c --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/core/PluginLoader.kt @@ -0,0 +1,121 @@ +package com.tencent.devops.plugin.core + +import com.tencent.devops.plugin.api.EXTENSION_LOCATION +import com.tencent.devops.plugin.api.ExtensionType +import com.tencent.devops.plugin.api.PluginInfo +import com.tencent.devops.plugin.api.PluginMetadata +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.security.MessageDigest +import java.util.LinkedList +import java.util.Properties +import java.util.jar.JarFile + +/** + * 插件加载器 + */ +class PluginLoader( + private val pluginPath: Path +) { + + val classLoader = PluginClassLoader(pluginPath, javaClass.classLoader) + + init { + check(Files.exists(pluginPath)) { "Plugin file[$pluginPath] does not exist." } + } + + fun loadPlugin(): PluginInfo { + JarFile(pluginPath.toFile()).use { + val digest = calculateDigest() + val metadata = resolveMetadata(it) + val extensions = resolveExtensions(it) + return PluginInfo( + id = metadata.id, + metadata = metadata, + digest = digest, + extensionPoints = extensions[ExtensionType.POINT].orEmpty(), + extensionControllers = extensions[ExtensionType.CONTROLLER].orEmpty() + ) + } + } + + private fun resolveExtensions(jarFile: JarFile): HashMap> { + try { + val result = HashMap>() + val properties = Properties() + val jarEntry = jarFile.getJarEntry(EXTENSION_LOCATION) + check(jarEntry != null) { "[$EXTENSION_LOCATION] does not exist in plugin [$pluginPath]" } + jarFile.getInputStream(jarEntry).use { + properties.load(it) + } + ExtensionType.values().forEach { type -> + val list = result.getOrPut(type) { LinkedList() } + properties.getProperty(type.identifier).orEmpty() + .split(",") + .filter { it.isNotBlank() } + .forEach { list.add(it.trim()) } + } + return result + } catch (ex: IOException) { + throw IllegalArgumentException("Unable to load extensions from location [$EXTENSION_LOCATION]", ex) + } + } + + private fun resolveMetadata(jarFile: JarFile): PluginMetadata { + try { + val manifest = jarFile.manifest + check(manifest != null) { "[$MANIFEST_LOCATION] does not exist in plugin [$pluginPath]" } + val attributes = manifest.mainAttributes + val id = attributes.getValue(PLUGIN_ID).orEmpty().trim() + check(id.isNotEmpty()) { "Required manifest attribute $PLUGIN_ID is null" } + val version = attributes.getValue(PLUGIN_VERSION).orEmpty().trim() + val scope = resolveScope(attributes.getValue(PLUGIN_SCOPE).orEmpty().trim()) + val author = attributes.getValue(PLUGIN_AUTHOR).orEmpty().trim() + val description = attributes.getValue(PLUGIN_DESCRIPTION).orEmpty().trim() + return PluginMetadata( + id = id, + name = id, + version = version, + scope = scope, + author = author, + description = description + ) + } catch (ex: IOException) { + throw IllegalArgumentException("Unable to load manifest from location [$MANIFEST_LOCATION]", ex) + } + } + + private fun resolveScope(value: String): List { + if (value.isEmpty() || value == "*") { + return emptyList() + } + val scope = value.split(",") + .map { it.trim() } + .filter { it.isNotBlank() } + .distinct() + return if (scope.contains("*")) emptyList() else scope + } + + private fun calculateDigest(): String { + val digest = MessageDigest.getInstance("SHA-256") + pluginPath.toFile().inputStream().use { input -> + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var sizeRead = input.read(buffer) + while (sizeRead != -1) { + digest.update(buffer, 0, sizeRead) + sizeRead = input.read(buffer) + } + return digest.digest().fold("") { str, it -> str + "%02x".format(it) } + } + } + + companion object { + private const val MANIFEST_LOCATION = "META-INF/MANIFEST.MF" + private const val PLUGIN_ID = "Plugin-Id" + private const val PLUGIN_VERSION = "Plugin-Version" + private const val PLUGIN_SCOPE = "Plugin-Scope" + private const val PLUGIN_AUTHOR = "Plugin-Author" + private const val PLUGIN_DESCRIPTION = "Plugin-Description" + } +} diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/spring/PluginEndpoint.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/spring/PluginEndpoint.kt new file mode 100644 index 0000000..fe4947d --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/spring/PluginEndpoint.kt @@ -0,0 +1,70 @@ +package com.tencent.devops.plugin.spring + +import com.tencent.devops.plugin.api.PluginInfo +import com.tencent.devops.plugin.api.PluginManager +import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation +import org.springframework.boot.actuate.endpoint.annotation.Endpoint +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation +import org.springframework.boot.actuate.endpoint.annotation.Selector +import org.springframework.boot.actuate.endpoint.annotation.WriteOperation +import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse +import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse.STATUS_INTERNAL_SERVER_ERROR + +/** + * 插件相关endpoint + * 访问地址 {host}/actuator/plugin + */ +@Endpoint(id = "plugin") +class PluginEndpoint( + private val pluginManager: PluginManager +) { + + /** + * 读取所有已加载的插件列表 + */ + @ReadOperation + fun list(): Map { + return pluginManager.getPluginMap() + } + + /** + * 重新加载所有插件 + */ + @WriteOperation + fun load(): WebEndpointResponse { + return try { + pluginManager.load() + WebEndpointResponse("ok") + } catch (ignored: Exception) { + WebEndpointResponse(ignored.message.orEmpty(), STATUS_INTERNAL_SERVER_ERROR) + } + } + + /** + * 加载指定插件 + * @param id 需要加载的插件id + */ + @WriteOperation + fun load(@Selector id: String): WebEndpointResponse { + return try { + pluginManager.load(id) + WebEndpointResponse("ok") + } catch (ignored: Exception) { + WebEndpointResponse(ignored.message.orEmpty(), STATUS_INTERNAL_SERVER_ERROR) + } + } + + /** + * 卸载指定插件 + * @param id 需要卸载的插件id + */ + @DeleteOperation + fun unload(@Selector id: String): WebEndpointResponse { + return try { + pluginManager.unload(id) + WebEndpointResponse("ok") + } catch (ignored: Exception) { + WebEndpointResponse(ignored.message.orEmpty(), STATUS_INTERNAL_SERVER_ERROR) + } + } +} diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/spring/PluginEndpointAutoConfiguration.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/spring/PluginEndpointAutoConfiguration.kt new file mode 100644 index 0000000..19fbc47 --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/spring/PluginEndpointAutoConfiguration.kt @@ -0,0 +1,17 @@ +package com.tencent.devops.plugin.spring + +import com.tencent.devops.plugin.api.PluginManager +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration +import org.springframework.boot.autoconfigure.AutoConfigureAfter +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(name = ["org.springframework.boot.actuate.endpoint.annotation.Endpoint"]) +@AutoConfigureAfter(EndpointAutoConfiguration::class) +class PluginEndpointAutoConfiguration { + + @Bean + fun pluginEndpoint(pluginManager: PluginManager) = PluginEndpoint(pluginManager) +} diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/spring/SpringExtensionRegistry.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/spring/SpringExtensionRegistry.kt new file mode 100644 index 0000000..9a2a853 --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/kotlin/com/tencent/devops/plugin/spring/SpringExtensionRegistry.kt @@ -0,0 +1,172 @@ +package com.tencent.devops.plugin.spring + +import com.tencent.devops.plugin.api.ExtensionPoint +import com.tencent.devops.plugin.api.ExtensionRegistry +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.support.BeanDefinitionBuilder +import org.springframework.beans.factory.support.DefaultListableBeanFactory +import org.springframework.context.ApplicationContext +import org.springframework.context.ApplicationContextAware +import org.springframework.util.ClassUtils +import org.springframework.util.ReflectionUtils +import org.springframework.web.context.WebApplicationContext +import org.springframework.web.servlet.mvc.method.RequestMappingInfo +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping +import java.lang.reflect.Method + +@Suppress("UNCHECKED_CAST") +class SpringExtensionRegistry : ExtensionRegistry, ApplicationContextAware { + + private lateinit var applicationContext: ApplicationContext + private lateinit var beanFactory: DefaultListableBeanFactory + private lateinit var requestMappingHandlerMapping: RequestMappingHandlerMapping + private val detectHandlerMethods = findDetectHandlerMethods() + private val getMappingForMethod = findGetMappingForMethod() + private val pluginToExtensionControllerMap = mutableMapOf>>() + private val pluginToExtensionPointMap = mutableMapOf>>() + private val typeToExtensionPointMap = mutableMapOf>() + + override fun setApplicationContext(applicationContext: ApplicationContext) { + require(WebApplicationContext::class.java.isAssignableFrom(applicationContext::class.java)) + this.applicationContext = applicationContext + this.beanFactory = applicationContext.autowireCapableBeanFactory as DefaultListableBeanFactory + this.requestMappingHandlerMapping = applicationContext.getBean(RequestMappingHandlerMapping::class.java) + } + + override fun registerExtensionController(plugin: String, name: String, type: Class<*>) { + registerBean(name, type) + registerHandlerMethods(name) + pluginToExtensionControllerMap.getOrPut(plugin) { mutableListOf() }.add(type) + logger.info("Register extension controller [$name]") + } + + override fun registerExtensionPoint(plugin: String, name: String, type: Class<*>) { + unregisterExtensionPoint(plugin, name) + + val instance = type.newInstance() + require(instance is ExtensionPoint) { "Extension Class must implement ExtensionPoint Interface" } + pluginToExtensionPointMap.getOrPut(plugin) { mutableMapOf() }.getOrPut(name) { mutableListOf() }.add(instance) + typeToExtensionPointMap.getOrPut(name) { mutableListOf() }.add(instance) + logger.info("Register extension point [$name]") + } + + override fun findExtensionPoints(type: Class): List { + return typeToExtensionPointMap[type.name].orEmpty().map { it as T } + } + + override fun unregisterExtensionPointsByPlugin(plugin: String) { + pluginToExtensionPointMap.remove(plugin)?.forEach { (type, extensions) -> + val existedExtensionMap = typeToExtensionPointMap[type] + extensions.forEach { existedExtensionMap?.remove(it) } + } + } + + override fun unregisterExtensionControllerByPlugin(plugin: String) { + pluginToExtensionControllerMap.remove(plugin)?.forEach { type -> + unregisterController(type.name, type) + unregisterBean(type.name) + } + } + + /** + * 注销已注册的ExtensionPoint + * @param plugin 插件名称 + * @param name 扩展point 名称 + */ + private fun unregisterExtensionPoint(plugin: String, name: String) { + val extensions = pluginToExtensionPointMap.getOrPut(plugin) { mutableMapOf() }.remove(name) + extensions?.forEach { typeToExtensionPointMap[name]?.remove(it) } + } + + /** + * 注销springmvc controller + * @param beanName: bean名称 + * @param type: bean class + */ + private fun unregisterController(beanName: String, type: Class<*>) { + ReflectionUtils.doWithMethods(type) { + try { + val specificMethod = ClassUtils.getMostSpecificMethod(it, type) + unregisterMapping(specificMethod, type) + } catch (ignored: Exception) { + logger.error("Failed to unregister request mapping[${it.name}]: $ignored", ignored) + } + } + logger.info("Unregister extension controller [$beanName]") + } + + /** + * 动态注册spring bean + * @param beanName: bean名称 + * @param type: bean type + */ + private fun registerBean(beanName: String, type: Class<*>) { + val beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(type) + beanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.beanDefinition) + } + + /** + * 注销spring bean + * @param beanName: bean名称 + */ + private fun unregisterBean(beanName: String) { + if (!beanFactory.containsBean(beanName)) { + return + } + beanFactory.removeBeanDefinition(beanName) + logger.info("Unregister extension bean [$beanName]") + } + + /** + * 使用反射调用detectHandlerMethods方法,检测并注册requestMapping + * @param beanName: controller bean name + */ + private fun registerHandlerMethods(beanName: String) { + // 注意: detectHandlerMethods(Object handler), handler可以为beanName,也可以为bean实例 + // 如果刚注册完bean, 这里传入beanName会报错: + // The mapped handler method class '' is not not an instance of the actual controller bean class, + // If the controller requires proxying (e.g. due to @Transactional), please use class-based proxying. + // 但注册完后获取一次bean就能正常运行,所以这里通过getBean获取一次bean实例并作为detectHandlerMethods的参数 + val instance = beanFactory.getBean(beanName) + detectHandlerMethods.invoke(requestMappingHandlerMapping, instance) + } + + /** + * 动态注销request handler mapping + * @param method: handler method + * @param targetClass: target class + */ + private fun unregisterMapping(method: Method, targetClass: Class<*>) { + getMappingForMethod.invoke(requestMappingHandlerMapping, method, targetClass)?.let { + require(it is RequestMappingInfo) + requestMappingHandlerMapping.unregisterMapping(it) + logger.debug("Unregister handler mapping ${it.patternsCondition?.patterns}") + } + } + + /** + * 通过反射寻找detectHandlerMethods方法 + * 因为可能存在自定义requestMappingHandlerMapping,所以使用递归判断 + */ + private fun findDetectHandlerMethods(): Method { + val parent = RequestMappingHandlerMapping::class.java.superclass.superclass + return parent.getDeclaredMethod(DETECT_HANDLER_METHODS_NAME, Any::class.java).apply { isAccessible = true } + } + + /** + * 通过反射寻找getMappingMethod方法 + */ + private fun findGetMappingForMethod(): Method { + return RequestMappingHandlerMapping::class.java.getDeclaredMethod( + GET_MAPPING_FOR_METHOD_NAME, + Method::class.java, + Class::class.java + ).apply { isAccessible = true } + } + + companion object { + private const val DETECT_HANDLER_METHODS_NAME = "detectHandlerMethods" + private const val GET_MAPPING_FOR_METHOD_NAME = "getMappingForMethod" + private val logger = LoggerFactory.getLogger(SpringExtensionRegistry::class.java) + } +} diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/resources/META-INF/spring.factories b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..6e4189c --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-core/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.tencent.devops.plugin.PluginAutoConfiguration,\ +com.tencent.devops.plugin.spring.PluginEndpointAutoConfiguration diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-processor/build.gradle.kts b/devops-boot-project/devops-boot-core/devops-plugin/plugin-processor/build.gradle.kts new file mode 100644 index 0000000..39c868c --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-processor/build.gradle.kts @@ -0,0 +1,6 @@ +description = "DevOps Boot Plugin Processor" + +dependencies { + implementation(project(":devops-boot-project:devops-boot-core:devops-plugin:plugin-api")) + implementation("org.springframework:spring-webmvc") +} diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-processor/src/main/kotlin/com/tencent/devops/plugin/processor/ExtensionAnnotationProcessor.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-processor/src/main/kotlin/com/tencent/devops/plugin/processor/ExtensionAnnotationProcessor.kt new file mode 100644 index 0000000..76c27c1 --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-processor/src/main/kotlin/com/tencent/devops/plugin/processor/ExtensionAnnotationProcessor.kt @@ -0,0 +1,86 @@ +package com.tencent.devops.plugin.processor + +import com.tencent.devops.plugin.api.EXTENSION_LOCATION +import com.tencent.devops.plugin.api.Extension +import com.tencent.devops.plugin.api.ExtensionType +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.RestController +import java.io.IOException +import javax.annotation.processing.AbstractProcessor +import javax.annotation.processing.ProcessingEnvironment +import javax.annotation.processing.RoundEnvironment +import javax.lang.model.SourceVersion +import javax.lang.model.element.Element +import javax.lang.model.element.ElementKind +import javax.lang.model.element.TypeElement +import javax.tools.Diagnostic +import javax.tools.StandardLocation + +/** + * [Extension]注解处理器,将标记了[Extension]的类自动写入extensions.properties + */ +class ExtensionAnnotationProcessor : AbstractProcessor() { + + private val extensions = mutableMapOf>() + + override fun init(processingEnv: ProcessingEnvironment) { + super.init(processingEnv) + } + + override fun getSupportedAnnotationTypes(): MutableSet { + return mutableSetOf(Extension::class.java.canonicalName) + } + + override fun getSupportedSourceVersion(): SourceVersion? { + return SourceVersion.latestSupported() + } + + override fun process(annotations: MutableSet, roundEnv: RoundEnvironment): Boolean { + if (roundEnv.processingOver()) { + generateExtensionFile() + } else { + roundEnv.getElementsAnnotatedWith(Extension::class.java).forEach { + processExtensionElement(it) + } + } + return false + } + + private fun processExtensionElement(element: Element) { + log("Found @Extension element: $element") + if (element.kind != ElementKind.CLASS || element !is TypeElement) { + error("Invalid element type, class expected", element) + return + } + val type = determineExtensionType(element) + extensions.getOrPut(type) { mutableSetOf() }.add(element.qualifiedName.toString()) + } + + private fun determineExtensionType(element: Element): ExtensionType { + if (element.getAnnotation(Controller::class.java) != null || + element.getAnnotation(RestController::class.java) != null + ) { + return ExtensionType.CONTROLLER + } + return ExtensionType.POINT + } + + private fun generateExtensionFile() { + val filter = processingEnv.filer + try { + val extensionFile = filter.createResource(StandardLocation.CLASS_OUTPUT, "", EXTENSION_LOCATION) + ExtensionFiles.write(extensions, extensionFile.openOutputStream()) + log("Success to generate extension factories file") + } catch (exception: IOException) { + error(exception.message.orEmpty()) + } + } + + private fun log(message: String) { + processingEnv.messager.printMessage(Diagnostic.Kind.NOTE, message) + } + + private fun error(message: String, element: Element? = null) { + processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, message, element) + } +} diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-processor/src/main/kotlin/com/tencent/devops/plugin/processor/ExtensionFiles.kt b/devops-boot-project/devops-boot-core/devops-plugin/plugin-processor/src/main/kotlin/com/tencent/devops/plugin/processor/ExtensionFiles.kt new file mode 100644 index 0000000..b9cc9ae --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-processor/src/main/kotlin/com/tencent/devops/plugin/processor/ExtensionFiles.kt @@ -0,0 +1,29 @@ +package com.tencent.devops.plugin.processor + +import com.tencent.devops.plugin.api.ExtensionType +import java.io.BufferedWriter +import java.io.OutputStream +import java.io.OutputStreamWriter +import java.nio.charset.StandardCharsets.UTF_8 + +/** + * Extension配置文件工具类 + */ +object ExtensionFiles { + + /** + * 将扩展类名写入文件 + */ + fun write(factories: MutableMap>, output: OutputStream) { + BufferedWriter(OutputStreamWriter(output, UTF_8)).use { writer -> + writer.write("# Generated by DevOps Boot\n") + factories.forEach { (key, value) -> + writer.write(key.identifier) + writer.write("=\\\n ") + val names = value.joinToString(",\\\n ") + writer.write(names) + writer.newLine() + } + } + } +} diff --git a/devops-boot-project/devops-boot-core/devops-plugin/plugin-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/devops-boot-project/devops-boot-core/devops-plugin/plugin-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000..61731bb --- /dev/null +++ b/devops-boot-project/devops-boot-core/devops-plugin/plugin-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +com.tencent.devops.plugin.processor.ExtensionAnnotationProcessor diff --git a/devops-boot-project/devops-boot-starters/devops-boot-starter-plugin/build.gradle.kts b/devops-boot-project/devops-boot-starters/devops-boot-starter-plugin/build.gradle.kts new file mode 100644 index 0000000..8ecd59c --- /dev/null +++ b/devops-boot-project/devops-boot-starters/devops-boot-starter-plugin/build.gradle.kts @@ -0,0 +1,5 @@ +description = "Starter for DevOps Boot Plugin" + +dependencies { + api(project(":devops-boot-project:devops-boot-core:devops-plugin:plugin-core")) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 84f5114..acf5aa7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,19 +2,17 @@ rootProject.name = "devops-framework" fun File.directories() = listFiles()?.filter { it.isDirectory && it.name != "build" }?.toList() ?: emptyList() -include("devops-boot-project:devops-boot-core") -include("devops-boot-project:devops-boot-dependencies") -include("devops-boot-project:devops-boot-starters") -include("devops-boot-project:devops-boot-tools") - -file("$rootDir/devops-boot-project/devops-boot-core").directories().forEach { - include("devops-boot-project:devops-boot-core:${it.name}") +fun includeAll(module: String) { + include(module) + val name = module.replace(":", "/") + file("$rootDir/$name/").directories().forEach { + include("$module:${it.name}") + } } -file("$rootDir/devops-boot-project/devops-boot-starters").directories().forEach { - include("devops-boot-project:devops-boot-starters:${it.name}") -} +includeAll("devops-boot-project:devops-boot-core") +includeAll("devops-boot-project:devops-boot-dependencies") +includeAll("devops-boot-project:devops-boot-starters") +includeAll("devops-boot-project:devops-boot-tools") -file("$rootDir/devops-boot-project/devops-boot-tools").directories().forEach { - include("devops-boot-project:devops-boot-tools:${it.name}") -} +includeAll("devops-boot-project:devops-boot-core:devops-plugin") From 979f9099edaff1c9fbc7de129b594e87e1a03784 Mon Sep 17 00:00:00 2001 From: carrypan Date: Tue, 22 Jun 2021 15:32:14 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=A8=E6=96=87?= =?UTF-8?q?=E6=A1=A3=20#65?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 85 +++++++++---------- docs/_sidebar.md | 6 ++ .../dependency/devops-boot-dependencies.md | 7 +- docs/dependency/versions.md | 3 + .../devops-boot-starter-circuitbreaker.md | 27 ++++++ .../devops-boot-starter-loadbalancer.md | 27 ++++++ docs/starter/devops-boot-starter-plugin.md | 26 ++++++ docs/starter/devops-boot-starter-web.md | 4 +- 8 files changed, 134 insertions(+), 51 deletions(-) rename devops-boot-project/devops-boot-dependencies/README.md => docs/dependency/devops-boot-dependencies.md (77%) create mode 100644 docs/dependency/versions.md create mode 100644 docs/starter/devops-boot-starter-circuitbreaker.md create mode 100644 docs/starter/devops-boot-starter-loadbalancer.md create mode 100644 docs/starter/devops-boot-starter-plugin.md diff --git a/README.md b/README.md index 99ee2bf..f624a1e 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,49 @@ -# devops-framework -![GitHub](https://img.shields.io/github/license/bkdevops-projects/devops-framework) -![Maven Central](https://img.shields.io/maven-central/v/com.tencent.devops/devops-boot) -![GitHub Workflow Status (event)](https://img.shields.io/github/workflow/status/bkdevops-projects/devops-framework/build) +

DevOps Boot

+

基于Spring Boot的微服务快速开发框架

+
+[![GitHub](https://img.shields.io/github/license/bkdevops-projects/devops-framework)](https://img.shields.io/github/license/bkdevops-projects/devops-framework) +[![Maven Central](https://img.shields.io/maven-central/v/com.tencent.devops/devops-boot)](https://img.shields.io/maven-central/v/com.tencent.devops/devops-boot) +[![GitHub Workflow Status (event)](https://img.shields.io/github/workflow/status/bkdevops-projects/devops-framework/build)](https://img.shields.io/github/workflow/status/bkdevops-projects/devops-framework/build) -`devops-framework`是一款基于`Spring Boot`的微服务快速开发框架,提炼自腾讯DevOps团队内部多个项目,使用约定优于配置的设计理念,帮助我们专注于`DevOps`业务快速开发。 +
-## 项目特点 +
+ +[中文文档地址](https://bkdevops-projects.github.io/devops-framework/) + +
+ +---------- + +## DevOps Boot 是什么? + +`devops-boot`提炼自腾讯DevOps团队内部多个项目,使用约定优于配置的设计理念,帮助我们专注于DevOps业务快速开发,它具有以下优势: + +- **简单** :几乎零配置快速开发微服务,低成本上手 +- **易用** :采用`Spring Boot`组件化思想,易于学习理解 +- **统一** :目前已集成了微服务开发常用组件和统一配置 +- **扩展** :组件之间低耦合,高内聚,扩展十分方便 + +查看[快速开始](quick-start.md)了解详情。 + +## DevOps Boot 能解决什么问题? + +- **统一项目配置** : 免去繁琐的项目配置,gradle插件帮您解决烦恼 +- **统一依赖版本管理** : 多个项目统一jdk和三方依赖版本,避免版本冲突 +- **统一微服务治理解决方案**: 解决多个项目技术方案参差不齐,架构不统一问题 +- **统一常用工具类** : 避免代码重复 + +## 功能特性 - 提供gradle快速开发插件[devops-boot-gradle-plugin](./devops-boot-project/devops-boot-tools/devops-boot-gradle-plugin/README.md) - 提供gradle快速发布插件[devops-publish-gradle-plugin](./devops-boot-project/devops-boot-tools/devops-publish-gradle-plugin/README.md) - 提供统一版本依赖管理[devops-boot-dependencies](./devops-boot-project/devops-boot-dependencies/README.md) - 提供多个开箱即用的starter组件 - - [starter-logging](./devops-boot-project/devops-boot-starters/devops-boot-starter-logging/README.md) - [starter-api](./devops-boot-project/devops-boot-starters/devops-boot-starter-api/README.md) + - [starter-logging](./devops-boot-project/devops-boot-starters/devops-boot-starter-logging/README.md) - [starter-web](./devops-boot-project/devops-boot-starters/devops-boot-starter-web/README.md) - [starter-service](./devops-boot-project/devops-boot-starters/devops-boot-starter-service/README.md) - - TODO - -## 快速开始 -- **gradle.build.kts** -```groovy -// 添加devops-boot gradle插件 -plugins { - id("com.tencent.devops.boot") version ${version} -} - -dependencies { - // 添加需要的starter组件 - implementation("com.tencent.devops:devops-boot-starter-web") -} -``` -只需要添加`devops-boot`插件,就自动为我们配置好`jdk`版本、编译选项、依赖管理、`kotlin`依赖及`kotlin-spring`插件等等繁琐的配置项。 - -接下来即可直接开始业务逻辑代码的编写了。 - - -## 工程结构 -```shell script -devops-framework/ -├── buildSrc # gradle项目构建目录 -├── devops-boot-project # devops-boot源码目录 -│   ├── devops-boot-core # 核心模块 -│   ├── devops-boot-dependencies # maven bom模块 -│   ├── devops-boot-starters # starter组件目录 -│   └── devops-boot-tools # gradle脚本等工具目录 -├── devops-boot-sample # sample项目 -└── docs # 开发文档 -``` + - ... ## 核心依赖 @@ -55,11 +52,9 @@ devops-framework/ | JDK | 1.8+ | | Kotlin | 1.4.32 | | Gradle | 6.8.3 | -| Spring Boot | 2.3.7.RELEASE | -| Spring Cloud | Hoxton.SR9 | +| Spring Boot | 2.4.5 | +| Spring Cloud | 2020.0.2 | +## 示例 -## 发行版本 -- 0.0.1 2020年10月9日 -- 0.0.2 2020年12月22日 -- 0.0.3 2021年1月5日 +可以查看[sample](https://github.com/bkdevops-projects/devops-framework/tree/master/devops-boot-sample)来了解如何优雅集成`devops-boot`框架。 diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 021fad0..01043de 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -9,6 +9,12 @@ - [starter-logging](/starter/devops-boot-starter-logging.md) - [starter-web](/starter/devops-boot-starter-web.md) - [starter-service](/starter/devops-boot-starter-service.md) + - [starter-circuitbreaker](/starter/devops-boot-starter-circuitbreaker.md) + - [starter-loadbalancer](/starter/devops-boot-starter-loadbalancer.md) + - [starter-plugin](/starter/devops-boot-starter-plugin.md) +- **依赖管理** + - [devops-boot-dependencies](/dependency/devops-boot-dependencies.md) + - [版本列表](/dependency/versions.md) - [常见问题](/faq.md) - [参与开发](/contribute.md) - [更新日志](/changelog.md) diff --git a/devops-boot-project/devops-boot-dependencies/README.md b/docs/dependency/devops-boot-dependencies.md similarity index 77% rename from devops-boot-project/devops-boot-dependencies/README.md rename to docs/dependency/devops-boot-dependencies.md index dac865b..8a0ae17 100644 --- a/devops-boot-project/devops-boot-dependencies/README.md +++ b/docs/dependency/devops-boot-dependencies.md @@ -7,7 +7,7 @@ ## 使用方式 ### 1. 配合devops-boot插件使用(推荐) -当项目引入了`devops-boot-gradle-plugin`插件,会自动帮我们配置`devops-boot-dependencies`,无需任何配置 +当项目引入了`devops-boot-gradle-plugin`插件,会自动配置`devops-boot-dependencies`,无需其它额外配置 ### 2. 独立使用 @@ -31,10 +31,7 @@ dependencyManagement { } ``` -## 依赖版本列表 -请参考[build.gradle.kts](./build.gradle.kts)的`constraints`列表 - -### 参考 +## 参考 关于依赖管理的使用详情,请参考官方文档[dependency-management](https://docs.spring.io/dependency-management-plugin/docs/current/reference/html/) diff --git a/docs/dependency/versions.md b/docs/dependency/versions.md new file mode 100644 index 0000000..1520131 --- /dev/null +++ b/docs/dependency/versions.md @@ -0,0 +1,3 @@ +# versions + +?> 请参考[build.gradle.kts](https://github.com/bkdevops-projects/devops-framework/blob/master/devops-boot-project/devops-boot-dependencies/build.gradle.kts)的`constraints`列表 diff --git a/docs/starter/devops-boot-starter-circuitbreaker.md b/docs/starter/devops-boot-starter-circuitbreaker.md new file mode 100644 index 0000000..d4b0cbc --- /dev/null +++ b/docs/starter/devops-boot-starter-circuitbreaker.md @@ -0,0 +1,27 @@ +# devops-boot-starter-circuitbreaker + +`starter-circuitbreaker`组件帮助开发者完成微服务客户端熔断器&限流的配置 + +## 功能介绍 +?> 待完善 + +## 使用方式 +- **build.gradle.kts** + +```kotlin +implementation("com.tencent.devops:devops-boot-starter-circuitbreaker") +``` + +- **build.gradle** + +```groovy +implementation 'com.tencent.devops:devops-boot-starter-circuitbreaker' +``` + +## 配置属性 + +| 属性 | 类型 | 默认值 | 说明 | +| ------------------ | ------- | ------ | ------------------ | + +## 说明 + diff --git a/docs/starter/devops-boot-starter-loadbalancer.md b/docs/starter/devops-boot-starter-loadbalancer.md new file mode 100644 index 0000000..906c3f6 --- /dev/null +++ b/docs/starter/devops-boot-starter-loadbalancer.md @@ -0,0 +1,27 @@ +# devops-boot-starter-loadbalancer + +`starter-loadbalancer`组件帮助开发者完成微服务客户端负载均衡的配置 + +## 功能介绍 +?> 待完善 + +## 使用方式 +- **build.gradle.kts** + +```kotlin +implementation("com.tencent.devops:devops-boot-starter-loadbalancer") +``` + +- **build.gradle** + +```groovy +implementation 'com.tencent.devops:devops-boot-starter-loadbalancer' +``` + +## 配置属性 + +| 属性 | 类型 | 默认值 | 说明 | +| ------------------ | ------- | ------ | ------------------ | + +## 说明 + diff --git a/docs/starter/devops-boot-starter-plugin.md b/docs/starter/devops-boot-starter-plugin.md new file mode 100644 index 0000000..0ba5c82 --- /dev/null +++ b/docs/starter/devops-boot-starter-plugin.md @@ -0,0 +1,26 @@ +# devops-boot-starter-plugin + +`starter-plugin`组件帮助开发者实现插件机制,动态扩展系统功能 + +## 功能介绍 +- 支持动态加载、卸载插件 +- 支持通过`actuator`管理插件 +- 支持扩展Point,动态添加逻辑 +- 支持扩展Controller,动态增加接口 + + +## 使用方式 +- **build.gradle.kts** + +```kotlin +implementation("com.tencent.devops:devops-boot-starter-plugin") +``` + +- **build.gradle** + +```groovy +implementation 'com.tencent.devops:devops-boot-starter-plugin' +``` + +## 插件编写 +?> 待完善 diff --git a/docs/starter/devops-boot-starter-web.md b/docs/starter/devops-boot-starter-web.md index e8362e5..05b4cad 100644 --- a/docs/starter/devops-boot-starter-web.md +++ b/docs/starter/devops-boot-starter-web.md @@ -31,7 +31,7 @@ implementation 'com.tencent.devops:devops-boot-starter-web' ## 配置属性 -- swagger配置过程中会读取以下配置 +swagger配置过程中会读取以下配置 | 属性 | 类型 | 默认值 | 说明 | | ------------------ | ------- | ------ | ------------------ | @@ -39,4 +39,6 @@ implementation 'com.tencent.devops:devops-boot-starter-web' | spring.application.desc | string | null | 应用描述,swagger会页面展示该值 | | spring.application.version | string | null | 应用版本,swagger会页面展示该值 | +## 参考 + - `springfox starter`[文档地址](http://springfox.github.io/springfox/docs/current/)