diff --git a/README.md b/README.md
index 99ee2bf..f624a1e 100644
--- a/README.md
+++ b/README.md
@@ -1,52 +1,49 @@
-# devops-framework
-
-
-
+
DevOps Boot
+基于Spring Boot的微服务快速开发框架
+
+[](https://img.shields.io/github/license/bkdevops-projects/devops-framework)
+[](https://img.shields.io/maven-central/v/com.tencent.devops/devops-boot)
+[](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/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/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/)
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")