feat(config): 重构配置管理

This commit is contained in:
AiKrai 2025-04-27 15:50:13 +08:00
parent e97f3f5519
commit e7016373c2
15 changed files with 306 additions and 122 deletions

3
.gitignore vendored
View File

@ -5,7 +5,8 @@ build/
!**/src/test/**/build/
/config
/gradle
log/
/logs
/.cursor
### IntelliJ IDEA ###
.idea

View File

@ -1,21 +1,20 @@
package app.config
import app.config.auth.JWTAuthProvider
import app.config.db.DbPoolProvider
import cn.hutool.core.lang.Snowflake
import cn.hutool.core.util.IdUtil
import com.google.inject.*
import com.google.inject.name.Names
import com.google.inject.AbstractModule
import com.google.inject.Guice
import com.google.inject.Injector
import com.google.inject.Singleton
import io.vertx.core.Vertx
import io.vertx.ext.auth.jwt.JWTAuth
import io.vertx.pgclient.PgBuilder
import io.vertx.pgclient.PgConnectOptions
import io.vertx.sqlclient.Pool
import io.vertx.sqlclient.PoolOptions
import io.vertx.sqlclient.SqlClient
import kotlinx.coroutines.CoroutineScope
import org.aikrai.vertx.config.Config
import org.aikrai.vertx.config.DefaultScope
import org.aikrai.vertx.db.tx.TxMgrHolder.initTxMgr
import org.aikrai.vertx.config.FrameworkConfigModule
object InjectConfig {
fun configure(vertx: Vertx): Injector {
@ -27,44 +26,21 @@ class InjectorModule(
private val vertx: Vertx,
) : AbstractModule() {
override fun configure() {
val pool = getDbPool().also { initTxMgr(it) }
val coroutineScope = DefaultScope(vertx)
// 1. 安装框架提供的配置模块
install(FrameworkConfigModule())
for ((key, value) in Config.getConfigMap()) {
bind(String::class.java).annotatedWith(Names.named(key)).toInstance(value.toString())
}
// 2. 绑定 Vertx 实例和 CoroutineScope
bind(Vertx::class.java).toInstance(vertx)
bind(CoroutineScope::class.java).toInstance(coroutineScope)
bind(CoroutineScope::class.java).toInstance(DefaultScope(vertx))
// 3. 绑定 Snowflake
bind(Snowflake::class.java).toInstance(IdUtil.getSnowflake())
// 4. 绑定数据库连接池 (使用 Provider 来延迟创建)
bind(Pool::class.java).toProvider(DbPoolProvider::class.java).`in`(Singleton::class.java)
bind(SqlClient::class.java).to(Pool::class.java) // 绑定 SqlClient 到 Pool
// 5. 绑定 JWTAuth
bind(JWTAuth::class.java).toProvider(JWTAuthProvider::class.java).`in`(Singleton::class.java)
// 绑定 DbPool 为单例
bind(Pool::class.java).toInstance(pool)
bind(SqlClient::class.java).toInstance(pool)
}
private fun getDbPool(): Pool {
// val type = configMap["databases.type"].toString()
// val name = configMap["databases.name"].toString()
// val host = configMap["databases.host"].toString()
// val port = configMap["databases.port"].toString()
// val user = configMap["databases.username"].toString()
// val password = configMap["databases.password"].toString()
// val dbMap = Config.getKey("databases") as Map<String, String>
val name = Config.getKey("databases.name").toString()
val host = Config.getKey("databases.host").toString()
val port = Config.getKey("databases.port").toString()
val user = Config.getKey("databases.username").toString()
val password = Config.getKey("databases.password").toString()
val poolOptions = PoolOptions().setMaxSize(10)
val clientOptions = PgConnectOptions()
.setHost(host)
.setPort(port.toInt())
.setDatabase(name)
.setUser(user)
.setPassword(password)
.setTcpKeepAlive(true)
return PgBuilder.pool().connectingTo(clientOptions).with(poolOptions).using(vertx).build()
}
}

View File

@ -2,22 +2,22 @@ package app.config.auth
import com.google.inject.Inject
import com.google.inject.Provider
import com.google.inject.name.Named
import io.vertx.core.Vertx
import io.vertx.ext.auth.PubSecKeyOptions
import io.vertx.ext.auth.jwt.JWTAuth
import io.vertx.ext.auth.jwt.JWTAuthOptions
import org.aikrai.vertx.config.JwtConfig
class JWTAuthProvider @Inject constructor(
private val vertx: Vertx,
@Named("jwt.key") private val key: String
private val jwtConfig: JwtConfig
) : Provider<JWTAuth> {
override fun get(): JWTAuth {
val options = JWTAuthOptions()
.addPubSecKey(
PubSecKeyOptions()
.setAlgorithm("HS256")
.setBuffer(key)
.setAlgorithm(jwtConfig.algorithm)
.setBuffer(jwtConfig.key)
)
return JWTAuth.create(vertx, options)
}

View File

@ -0,0 +1,43 @@
package app.config.db
import com.google.inject.Inject
import com.google.inject.Provider
import com.google.inject.Singleton
import io.vertx.core.Vertx
import io.vertx.pgclient.PgBuilder
import io.vertx.pgclient.PgConnectOptions
import io.vertx.sqlclient.Pool
import io.vertx.sqlclient.PoolOptions
import org.aikrai.vertx.config.DatabaseConfig
import org.aikrai.vertx.db.tx.TxMgrHolder
/**
* 数据库连接池提供者
*/
@Singleton
class DbPoolProvider @Inject constructor(
private val vertx: Vertx,
private val dbConfig: DatabaseConfig
) : Provider<Pool> {
override fun get(): Pool {
val poolOptions = PoolOptions().setMaxSize(dbConfig.maxPoolSize)
val clientOptions = PgConnectOptions()
.setHost(dbConfig.host)
.setPort(dbConfig.port)
.setDatabase(dbConfig.name)
.setUser(dbConfig.username)
.setPassword(dbConfig.password)
.setTcpKeepAlive(true)
val pool = PgBuilder.pool()
.connectingTo(clientOptions)
.with(poolOptions)
.using(vertx)
.build()
// 初始化事务管理器
TxMgrHolder.initTxMgr(pool)
return pool
}
}

View File

@ -59,7 +59,7 @@ class Demo1Controller @Inject constructor(
suspend fun testRetriever(
@D("key", "key") key: String
) {
val configMap = Config.getKey(key)
val configMap = Config.getStringOrNull(key)
println(configMap)
}
}

View File

@ -2,24 +2,25 @@ package app.port.aipfox
import app.util.openapi.OpenApiSpecGenerator
import com.google.inject.Inject
import com.google.inject.name.Named
import io.vertx.core.Vertx
import io.vertx.core.http.HttpMethod
import io.vertx.core.json.JsonObject
import io.vertx.ext.web.client.WebClient
import io.vertx.ext.web.client.WebClientOptions
import mu.KotlinLogging
import org.aikrai.vertx.config.Config
class ApifoxClient @Inject constructor(
private val vertx: Vertx,
@Named("apifox.token") private val token: String,
@Named("apifox.projectId") private val projectId: String,
@Named("apifox.folderId") private val folderId: String,
@Named("server.name") private val serverName: String,
@Named("server.port") private val port: String
) {
private val logger = KotlinLogging.logger { }
private val token = Config.getString("apifox.token", "")
private val projectId = Config.getString("apifox.projectId", "")
private val folderId = Config.getString("apifox.folderId", "")
private val serverName = Config.getString("server.name", "")
private val port = Config.getString("server.port", "")
fun importOpenapi() {
val openApiJsonStr = OpenApiSpecGenerator().genOpenApiSpecStr(serverName, "1.0", "http://127.0.0.1:$port/api")
val options = WebClientOptions().setDefaultPort(443).setDefaultHost("api.apifox.com").setSsl(true)

View File

@ -6,28 +6,23 @@ import io.vertx.core.Vertx
import io.vertx.kotlin.coroutines.coAwait
import io.vertx.redis.client.*
import mu.KotlinLogging
import org.aikrai.vertx.config.Config
import org.aikrai.vertx.config.RedisConfig
@Singleton
class RedisClient @Inject constructor(
vertx: Vertx
vertx: Vertx,
redisConfig: RedisConfig
) {
private val logger = KotlinLogging.logger { }
private val host = Config.getKey("redis.host").toString()
private val port = Config.getKey("redis.port").toString()
private val database = Config.getKey("redis.database").toString().toInt()
private val password = Config.getKey("redis.password").toString()
private val maxPoolSize = Config.getKey("redis.maxPoolSize").toString().toInt()
private val maxPoolWaiting = Config.getKey("redis.maxPoolWaiting").toString().toInt()
private var redisClient = Redis.createClient(
vertx,
RedisOptions()
.setType(RedisClientType.STANDALONE)
.addConnectionString("redis://$host:$port/$database")
.setPassword(password)
.setMaxPoolSize(maxPoolSize)
.setMaxPoolWaiting(maxPoolWaiting)
.addConnectionString("redis://${redisConfig.host}:${redisConfig.port}/${redisConfig.db}")
.setPassword(redisConfig.pass ?: "")
.setMaxPoolSize(redisConfig.poolSize)
.setMaxPoolWaiting(redisConfig.maxPoolWaiting)
)
// EX秒PX毫秒

View File

@ -9,7 +9,6 @@ import app.port.aipfox.ApifoxClient
import cn.hutool.core.lang.Snowflake
import com.google.inject.Inject
import com.google.inject.Injector
import com.google.inject.name.Named
import io.vertx.core.Handler
import io.vertx.core.http.HttpHeaders
import io.vertx.core.http.HttpMethod
@ -23,7 +22,7 @@ import io.vertx.kotlin.coroutines.coAwait
import kotlinx.coroutines.CoroutineScope
import mu.KotlinLogging
import org.aikrai.vertx.auth.AuthUser
import org.aikrai.vertx.config.Config
import org.aikrai.vertx.config.ServerConfig
import org.aikrai.vertx.context.RouterBuilder
import org.aikrai.vertx.jackson.JsonUtil
import org.aikrai.vertx.utlis.LangUtil.toStringMap
@ -36,8 +35,7 @@ class WebVerticle @Inject constructor(
private val apifoxClient: ApifoxClient,
private val snowflake: Snowflake,
private val responseHandler: ResponseHandler,
@Named("server.port") private val port: Int,
@Named("server.context") private val context: String,
private val serverConfig: ServerConfig
) : CoroutineVerticle() {
private val logger = KotlinLogging.logger { }
@ -48,30 +46,29 @@ class WebVerticle @Inject constructor(
val options = HttpServerOptions().setMaxFormAttributeSize(1024 * 1024)
val server = vertx.createHttpServer(options)
.requestHandler(rootRouter)
.listen(port)
.listen(serverConfig.port)
.coAwait()
apifoxClient.importOpenapi()
logger.info { "http server start - http://127.0.0.1:${server.actualPort()}/$context" }
logger.info { "http server start - http://127.0.0.1:${server.actualPort()}${serverConfig.context}" }
}
override suspend fun stop() {
}
private fun setupRouter(rootRouter: Router, router: Router) {
rootRouter.route("/api" + "*").subRouter(router)
rootRouter.route("${serverConfig.context}*").subRouter(router)
router.route()
.handler(corsHandler)
.handler(BodyHandler.create())
.handler(logHandler)
.failureHandler(errorHandler)
val authHandler = JwtAuthenticationHandler(coroutineScope, tokenService, context, snowflake)
val authHandler = JwtAuthenticationHandler(coroutineScope, tokenService, serverConfig.context, snowflake)
router.route("/*").handler(authHandler)
val scanPath = Config.getKeyAsString("server.package")
val routerBuilder = RouterBuilder(coroutineScope, router, scanPath, responseHandler).build { service ->
val routerBuilder = RouterBuilder(coroutineScope, router, serverConfig.scanPackage, responseHandler).build { service ->
getIt.getInstance(service)
}
authHandler.anonymous.addAll(routerBuilder.anonymousPaths)

View File

@ -1,15 +1,5 @@
server:
name: vtx_demo
context: api # 上下文
timeout: 120 # eventbus超时时间
http:
header: # header获取到的变量
- x-requested-with
- Access-Control-Allow-Origin
- origin
- Content-Type
- accept
event-bus:
timeout: 10000 # 毫秒
jwt:
key: 123456sdfjasdfjl # jwt加密key
context: /api
# active: dev
package: app

View File

@ -0,0 +1,18 @@
application:
version: 1.0.0
server:
http:
header: # header获取到的变量
- x-requested-with
- Access-Control-Allow-Origin
- origin
- Content-Type
- accept
event-bus:
timeout: 10000 # 毫秒
jwt:
key: 123456sdfjasdfjl
algorithm: HS256
expiresInSeconds: 604800 # 7天

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration
debug="false" scan="true" scanPeriod="30 second">
<property name="ROOT" value="./log/"/>
<property name="ROOT" value="./logs/"/>
<property name="APPNAME" value="vertx-demo"/>
<property name="FILESIZE" value="500MB"/>
<property name="MAXHISTORY" value="100"/>

View File

@ -3,7 +3,6 @@ package app
import org.aikrai.vertx.db.annotation.TableField
import org.aikrai.vertx.db.annotation.TableId
import org.aikrai.vertx.db.annotation.TableName
import org.aikrai.vertx.db.annotation.TableIndex
import org.aikrai.vertx.db.annotation.EnumValue
import org.aikrai.vertx.db.annotation.TableFieldComment
import org.aikrai.vertx.db.migration.AnnotationMapping
@ -98,10 +97,10 @@ object GenerateMigration {
)
// 设置索引映射
mapper.indexMapping = AnnotationMapping(
annotationClass = TableIndex::class,
propertyName = "name"
)
// mapper.indexMapping = AnnotationMapping(
// annotationClass = TableIndex::class,
// propertyName = "name"
// )
// 设置枚举值映射
mapper.enumValueMapping = AnnotationMapping(

View File

@ -0,0 +1,53 @@
package org.aikrai.vertx.config
/**
* 数据库配置
*/
data class DatabaseConfig(
val name: String,
val host: String,
val port: Int,
val username: String,
val password: String,
val maxPoolSize: Int = 10
)
/**
* Redis配置
*/
data class RedisConfig(
val host: String,
val port: Int,
val db: Int,
val pass: String?,
val poolSize: Int = 8,
val maxPoolWaiting: Int = 32
)
/**
* JWT配置
*/
data class JwtConfig(
val key: String,
val algorithm: String = "HS256",
val expiresInSeconds: Int = 60 * 60 * 24 * 7 // 7天
)
/**
* 服务器配置
*/
data class ServerConfig(
val port: Int,
val context: String,
val scanPackage: String
)
/**
* 框架配置
*/
data class FrameworkConfiguration(
val server: ServerConfig,
val database: DatabaseConfig,
val redis: RedisConfig,
val jwt: JwtConfig
)

View File

@ -12,7 +12,7 @@ import java.util.concurrent.atomic.AtomicReference
object Config {
private val retriever = AtomicReference<ConfigRetriever?>(null)
private var configMap = emptyMap<String, Any>()
private val configMapRef = AtomicReference<Map<String, Any>>(emptyMap())
suspend fun init(vertx: Vertx) {
if (retriever.get() != null) return
@ -20,38 +20,71 @@ object Config {
val cas = retriever.compareAndSet(null, configRetriever)
if (cas) {
val configObj = configRetriever.config.coAwait()
configMap = FlattenUtil.flattenJsonObject(configObj)
// 存储扁平化的 Map
configMapRef.set(FlattenUtil.flattenJsonObject(configObj))
}
}
fun getKey(key: String): Any? {
if (retriever.get() == null) throw IllegalStateException("Config not initialized")
// 检查 configMap 中是否存在指定的 key
return if (configMap.containsKey(key)) {
configMap[key]
} else {
// 找到所有以 key 开头的条目
val map = configMap.filterKeys { it.startsWith(key) }
// 如果没有找到任何匹配的条目,返回 null
return map.ifEmpty { null }
}
fun getString(key: String, defaultValue: String): String {
return configMapRef.get()[key]?.toString() ?: defaultValue
}
fun getKeyAsString(key: String): String? {
if (retriever.get() == null) throw IllegalStateException("Config not initialized")
// 检查 configMap 中是否存在指定的 key
return if (configMap.containsKey(key)) {
configMap[key].toString()
} else {
// 找到所有以 key 开头的条目
val map = configMap.filterKeys { it.startsWith(key) }
// 如果没有找到任何匹配的条目,返回 null
if (map.isEmpty()) return null else map.toString()
}
fun getStringOrNull(key: String): String? {
return configMapRef.get()[key]?.toString()
}
fun getInt(key: String, defaultValue: Int): Int {
return configMapRef.get()[key]?.toString()?.toIntOrNull() ?: defaultValue
}
fun getIntOrNull(key: String): Int? {
return configMapRef.get()[key]?.toString()?.toIntOrNull()
}
fun getLong(key: String, defaultValue: Long): Long {
return configMapRef.get()[key]?.toString()?.toLongOrNull() ?: defaultValue
}
fun getLongOrNull(key: String): Long? {
return configMapRef.get()[key]?.toString()?.toLongOrNull()
}
fun getBoolean(key: String, defaultValue: Boolean): Boolean {
val value = configMapRef.get()[key]
return when (value) {
is Boolean -> value
is String -> value.toBooleanStrictOrNull() ?: defaultValue // toBooleanStrictOrNull 更安全
else -> defaultValue
}
}
fun getBooleanOrNull(key: String): Boolean? {
val value = configMapRef.get()[key]
return when (value) {
is Boolean -> value
is String -> value.toBooleanStrictOrNull()
else -> null
}
}
// 获取嵌套对象或列表
fun getObject(keyPrefix: String): Map<String, Any>? {
val map = configMapRef.get()
val subMap = map.filterKeys { it.startsWith("$keyPrefix.") }
.mapKeys { it.key.removePrefix("$keyPrefix.") }
return if (subMap.isEmpty()) null else subMap
}
fun getStringList(key: String, defaultValue: List<String> = emptyList()): List<String> {
return (configMapRef.get()[key] as? JsonArray)?.mapNotNull { it?.toString() } ?: defaultValue
}
fun getStringListOrNull(key: String): List<String>? {
return (configMapRef.get()[key] as? JsonArray)?.mapNotNull { it?.toString() }
}
fun getConfigMap(): Map<String, Any> {
return configMap
return configMapRef.get()
}
private suspend fun load(vertx: Vertx): ConfigRetriever {

View File

@ -0,0 +1,78 @@
package org.aikrai.vertx.config
import com.google.inject.AbstractModule
import com.google.inject.Provides
import com.google.inject.Singleton
/**
* 框架配置模块
*
* 负责使用增强后的Config对象读取配置并将其实例化为数据类进行绑定
*/
class FrameworkConfigModule : AbstractModule() {
override fun configure() {
// 这里不需要bind(Config::class.java)因为Config是object
}
@Provides
@Singleton
fun provideDatabaseConfig(): DatabaseConfig {
return DatabaseConfig(
name = Config.getString("databases.name", "default_db"),
host = Config.getString("databases.host", "localhost"),
port = Config.getInt("databases.port", 5432),
username = Config.getString("databases.username", "user"),
password = Config.getString("databases.password", "password"),
maxPoolSize = Config.getInt("databases.maxPoolSize", 10)
)
}
@Provides
@Singleton
fun provideRedisConfig(): RedisConfig {
return RedisConfig(
host = Config.getString("redis.host", "localhost"),
port = Config.getInt("redis.port", 6379),
db = Config.getInt("redis.database", 0),
pass = Config.getStringOrNull("redis.password"),
poolSize = Config.getInt("redis.maxPoolSize", 8),
maxPoolWaiting = Config.getInt("redis.maxPoolWaiting", 32)
)
}
@Provides
@Singleton
fun provideJwtConfig(): JwtConfig {
val key = Config.getStringOrNull("jwt.key")
?: throw IllegalStateException("缺少必要配置: jwt.key")
return JwtConfig(
key = key,
algorithm = Config.getString("jwt.algorithm", "HS256"),
expiresInSeconds = Config.getInt("jwt.expiresInSeconds", 60 * 60 * 24 * 7)
)
}
@Provides
@Singleton
fun provideServerConfig(): ServerConfig {
val scanPackage = Config.getStringOrNull("server.package")
?: throw IllegalStateException("缺少必要配置: server.package")
return ServerConfig(
port = Config.getInt("server.port", 8080),
context = Config.getString("server.context", "/api"),
scanPackage = scanPackage
)
}
@Provides
@Singleton
fun provideFrameworkConfiguration(
server: ServerConfig,
database: DatabaseConfig,
redis: RedisConfig,
jwt: JwtConfig
): FrameworkConfiguration {
return FrameworkConfiguration(server, database, redis, jwt)
}
}