feat(webmvc): Add version control support

- Added ApiVersion annotations for version control of controllers and methods - Implement VersionAutoConfiguration automatic configuration class
- Add ApiVersionRequestMappingHandlerMapping to handle version mapping
- Update build configuration, support Spring Boot and version control
- Added Git-related configuration files
This commit is contained in:
gewuyou 2025-04-26 14:04:44 +08:00
parent 2e60505c20
commit c22389d584
17 changed files with 347 additions and 65 deletions

View File

@ -8,35 +8,35 @@ plugins {
// Kotlin Spring 支持
alias(libs.plugins.kotlin.plugin.spring)
}
//scmVersion {
// tag {
// prefix.set("") // 不加 v生成 1.0.1 而不是 v1.0.1
// }
// versionIncrementer("incrementMinorIfNotOnRelease")
// hooks {
// pre("fileUpdate", mapOf(
// "file" to "README.md",
// "pattern" to "version: (.*)",
// "replacement" to "version: \$version"
// ))
// pre("commit")
// post("push")
// }
//
//}
//version = scmVersion.version
scmVersion {
tag {
prefix.set("") // 不加 v生成 1.0.1 而不是 v1.0.1
}
versionIncrementer("incrementMinorIfNotOnRelease")
hooks {
pre(
"fileUpdate", mapOf(
"file" to "README.md",
"pattern" to "version: (.*)",
"replacement" to "version: \$version"
)
)
pre("commit")
post("push")
}
}
version = scmVersion.version
val configDir = "$rootDir/config/"
val tasksDir = "$configDir/tasks/"
//apply {
// from(file("$tasksDir/gradleTask.gradle"))
//
allprojects {
// 设置全局属性
ext {
set(ProjectFlags.IS_ROOT_MODULE, false)
set(ProjectFlags.USE_SPRING_BOOT_BOM, false)
set(ProjectFlags.USE_SPRING_BOOT_WEB, false)
}
afterEvaluate {
if (project.getPropertyByBoolean(ProjectFlags.IS_ROOT_MODULE)) {
@ -48,36 +48,87 @@ allprojects {
}
}
}
project.group = "com.gewuyou.forgeboot"
}
subprojects {
// afterEvaluate {
// if (project.getPropertyByBoolean(ProjectFlags.USE_GRPC)) {
// dependencies{
// implementation(platform(libs.grpc.bom))
// // gRPC Stub
// implementation(libs.grpc.stub)
// }
// }
// if (project.getPropertyByBoolean(ProjectFlags.USE_SPRING_BOOT)){
// dependencies{
// implementation(platform(libs.springBootDependencies.bom))
// compileOnly(libs.springBootStarter.web)
// }
// }
// }
version = rootProject.version
afterEvaluate {
if(project.getPropertyByBoolean(ProjectFlags.USE_SPRING_BOOT_BOM)){
dependencies{
implementation(platform(libs.springBootDependencies.bom))
}
}
if (project.getPropertyByBoolean(ProjectFlags.USE_SPRING_BOOT_WEB)){
dependencies{
compileOnly(libs.springBootStarter.web)
}
}
}
val libs = rootProject.libs
apply {
plugin(libs.plugins.java.get().pluginId)
plugin(libs.plugins.maven.publish.get().pluginId)
plugin(libs.plugins.kotlin.jvm.get().pluginId)
plugin(libs.plugins.axionRelease.get().pluginId)
// plugin(libs.plugins.spring.dependency.management.get().pluginId)
// 导入仓库配置
from(file("$configDir/repositories.gradle.kts"))
// 导入源代码任务
from(file("$tasksDir/sourceTask.gradle"))
from(file("$tasksDir/sourceTask.gradle.kts"))
// // 导入发布配置
// from(file("$configDir/publishing.gradle"))
// from(file("$configDir/publishing.gradle.kts"))
}
publishing {
repositories {
maven {
name = "localRepo"
url = uri("file://D:/Maven/mvn_repository")
}
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/GeWuYou/forgeboot")
credentials {
username = System.getenv("GITHUB_USERNAME")
password = System.getenv("GITHUB_TOKEN")
}
}
}
publications {
create<MavenPublication>("mavenJava") {
val projectName = project.name
from(components["java"])
groupId = project.group.toString()
artifactId = projectName
version = project.version.toString()
pom {
name.set(projectName)
description.set("Part of Forgeboot Starters")
url.set("https://github.com/GeWuYou/forgeboot")
licenses {
license {
name.set("Apache-2.0")
url.set("https://www.apache.org/licenses/LICENSE-2.0")
}
}
developers {
developer {
id.set("gewuyou")
name.set("gewuyou")
email.set("gewuyou1024@gmail.com")
}
}
scm {
connection.set("scm:git:git://github.com/GeWuYou/forgeboot.git")
developerConnection.set("scm:git:ssh://github.com/GeWuYou/forgeboot.git")
url.set("https://github.com/GeWuYou/forgeboot")
}
}
}
}
dependencies {
}
@ -86,11 +137,6 @@ subprojects {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
// kotlin {
// compilerOptions {
// freeCompilerArgs.addAll("-Xjsr305=strict")
// }
// }
tasks.withType<Jar> {
isEnabled = true
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
@ -100,6 +146,8 @@ subprojects {
}
}
}
fun Project.getPropertyByBoolean(key: String): Boolean {
return properties[key]?.toString()?.toBoolean() ?: false
}

View File

@ -8,6 +8,7 @@
object Modules {
object Webmvc {
const val STARTER = ":forgeboot-webmvc"
const val VERSION_STARTER = ":forgeboot-webmvc:forgeboot-webmvc-version-starter"
const val LOGGER_STARTER = ":forgeboot-webmvc:forgeboot-webmvc-logger-starter"
}

View File

@ -1,3 +1,5 @@
object ProjectFlags {
const val IS_ROOT_MODULE = "isRootModule"
const val USE_SPRING_BOOT_BOM = "useSpringBootBom"
const val USE_SPRING_BOOT_WEB = "useSpringBootWeb"
}

View File

View File

@ -1,17 +1,19 @@
// This task creates a jar file with the source code of the project
tasks.register("sourceTask", Jar, { Jar jar ->
tasks.register<Jar>("sourceTask") {
logger.info("正在配置${project.name}源代码 jar 文件...")
// 单独添加 Kotlin 源代码目录
from(sourceSets.main.kotlin.srcDirs) {
into("kotlin") // 将 Kotlin 源代码放入子目录 kotlin
// 收集所有源代码(包括 Kotlin 和 Java
val sourceSet = project.extensions.getByType<SourceSetContainer>()["main"]
from(sourceSet.allSource) {
into("sources") // 将所有源代码放入子目录 sources
}
archiveClassifier.set("sources") // 设置生成文件的分类标识
logger.info("正在创建${project.name}源代码 jar 文件...")
logger.info("创建${project.name}源代码 jar 文件完成!")
}).configure {
group = "build"
}.configure {
group = "source"
description = "使用项目的源代码创建源代码 jar 文件"
}

3
forgeboot-webmvc/.gitattributes vendored Normal file
View File

@ -0,0 +1,3 @@
/gradlew text eol=lf
*.bat text eol=crlf
*.jar binary

40
forgeboot-webmvc/.gitignore vendored Normal file
View File

@ -0,0 +1,40 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Kotlin ###
.kotlin

View File

@ -0,0 +1,8 @@
extra {
// 标记为根项目
setProperty(ProjectFlags.IS_ROOT_MODULE, true)
}
dependencies {
}

View File

@ -0,0 +1,3 @@
/gradlew text eol=lf
*.bat text eol=crlf
*.jar binary

View File

@ -0,0 +1,40 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Kotlin ###
.kotlin

View File

@ -0,0 +1,8 @@
extra {
// 标记为根项目
setProperty(ProjectFlags.USE_SPRING_BOOT_BOM, true)
setProperty(ProjectFlags.USE_SPRING_BOOT_WEB, true)
}
dependencies {
}

View File

@ -0,0 +1,16 @@
package com.gewuyou.forgeboot.webmvc.version.annotation
/**
*API 版本注解
* 可用于类或方法上用于标识 API 的版本号方法上的注解优先级高于类上的注解
* @since 2025-02-04 20:23:34
* @author gewuyou
*/
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class ApiVersion (
/**
* API 版本号值 ( v1, v2, v3 )默认值为 v1
*/
vararg val value: String = ["v1"]
)

View File

@ -0,0 +1,28 @@
package com.gewuyou.forgeboot.webmvc.version.config
import com.gewuyou.forgeboot.webmvc.version.mapping.ApiVersionRequestMappingHandlerMapping
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
/**
*版本自动配置
*
* @since 2025-04-26 13:09:58
* @author gewuyou
*/
@Configuration
open class VersionAutoConfiguration {
/**
* 创建并配置一个 ApiVersionRequestMappingHandlerMapping 实例
*
* 该方法通过注解 @Bean 标记表明此方法会返回一个由 Spring 管理的 Bean 实例
* ApiVersionRequestMappingHandlerMapping 是一个自定义的 HandlerMapping
* 它支持基于 API 版本的请求映射处理这对于需要版本控制的 RESTful API 设计非常有用
*
* @return ApiVersionRequestMappingHandlerMapping 实例用于处理基于 API 版本的请求映射
*/
@Bean
open fun apiVersionRequestMappingHandlerMapping(): ApiVersionRequestMappingHandlerMapping {
return ApiVersionRequestMappingHandlerMapping()
}
}

View File

@ -0,0 +1,79 @@
package com.gewuyou.forgeboot.webmvc.version.mapping
import com.gewuyou.forgeboot.webmvc.version.annotation.ApiVersion
import org.springframework.core.annotation.AnnotatedElementUtils
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.servlet.mvc.method.RequestMappingInfo
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
import java.lang.reflect.Method
/**
*API 版本请求映射处理程序映射
*
* @since 2025-02-04 20:30:44
* @author gewuyou
*/
class ApiVersionRequestMappingHandlerMapping : RequestMappingHandlerMapping() {
/**
* 判断是否处理特定类型的Bean
* 仅处理标注了 @RestController 注解的类
* @param beanType 要判断的Bean类型
* @return 如果类型标注了 @RestController则返回true否则返回false
*/
override fun isHandler(beanType: Class<*>): Boolean {
// 仅处理标注了 @RestController 注解的类
return AnnotatedElementUtils.hasAnnotation(beanType, RestController::class.java)
}
/**
* 获取方法的映射信息
* 首先尝试从方法上获取 @ApiVersion 注解如果不存在则尝试从类上获取
* 如果存在 @ApiVersion 注解则会根据注解中的版本信息来组合新地映射路径
* @param method 方法对象
* @param handlerType 处理器类型
* @return 可能包含版本信息的 RequestMappingInfo如果不存在 @ApiVersion 注解则返回原始的映射信息
*/
override fun getMappingForMethod(method: Method, handlerType: Class<*>): RequestMappingInfo? {
// 获取原始的 @RequestMapping 信息
val requestMappingInfo = super.getMappingForMethod(method, handlerType) ?: return null
// 优先获取方法上的注解
val methodApiVersion = AnnotatedElementUtils.findMergedAnnotation(method, ApiVersion::class.java)
// 当方法注解存在时构建并返回 RequestMappingInfo
methodApiVersion?.let {
return combineVersionMappings(requestMappingInfo, it.value)
}
// 获取类上的 @ApiVersion 注解
val classApiVersion = AnnotatedElementUtils.findMergedAnnotation(handlerType, ApiVersion::class.java)
classApiVersion?.let {
return combineVersionMappings(requestMappingInfo, it.value)
}
// 当类注解不存在时返回原始的 RequestMappingInfo
return requestMappingInfo
}
/**
* 组合版本路径支持多个版本
* @param originalMapping 原始的 RequestMappingInfo
* @param versions 版本数组
* @return 组合后的 RequestMappingInfo
*/
private fun combineVersionMappings(
originalMapping: RequestMappingInfo,
versions: Array<out String>
): RequestMappingInfo {
return versions
.map {
// 加上版本前缀
RequestMappingInfo
.paths("/api/$it")
.build()
}.reduce {
// 组合
acc, mapping ->
acc.combine(mapping)
}
// 组合原始 RequestMapping
.combine(originalMapping)
}
}

View File

@ -0,0 +1 @@
com.gewuyou.forgeboot.webmvc.version.config.VersionAutoConfiguration

View File

@ -15,6 +15,8 @@ kotlinxDatetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.r
kotlinxSerialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJSON-version" }
kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines-version" }
springBootStarter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" }
springBootDependencies-bom = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot-version" }
# Libraries can be bundled together for easier import
[bundles]

View File

@ -27,5 +27,6 @@ include(
":forgeboot-webmvc:forgeboot-webmvc-version-starter",
":forgeboot-webmvc:forgeboot-webmvc-logger-starter"
)
project(":forgeboot-webmvc:forgeboot-webmvc-version-starter").name = "forgeboot-webmvc-version-starter"
project(":forgeboot-webmvc:forgeboot-webmvc-logger-starter").name = "forgeboot-webmvc-logger-starter"
project(":forgeboot-webmvc").name = "forgeboot-webmvc-spring-boot-starter"
project(":forgeboot-webmvc:forgeboot-webmvc-version-starter").name = "forgeboot-webmvc-version-spring-boot-starter"
project(":forgeboot-webmvc:forgeboot-webmvc-logger-starter").name = "forgeboot-webmvc-logger-spring-boot-starter"