diff --git a/.gitignore b/.gitignore index a451bd6..718d1a4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,8 @@ build/ !**/src/test/**/build/ /config /gradle -log/ +/logs +/.cursor ### IntelliJ IDEA ### .idea diff --git a/vertx-demo/src/main/kotlin/app/config/InjectConfig.kt b/vertx-demo/src/main/kotlin/app/config/InjectConfig.kt index 690be67..bd72249 100644 --- a/vertx-demo/src/main/kotlin/app/config/InjectConfig.kt +++ b/vertx-demo/src/main/kotlin/app/config/InjectConfig.kt @@ -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 - 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() - } -} +} \ No newline at end of file diff --git a/vertx-demo/src/main/kotlin/app/config/auth/JWTAuthProvider.kt b/vertx-demo/src/main/kotlin/app/config/auth/JWTAuthProvider.kt index ab096b3..8fda9b2 100644 --- a/vertx-demo/src/main/kotlin/app/config/auth/JWTAuthProvider.kt +++ b/vertx-demo/src/main/kotlin/app/config/auth/JWTAuthProvider.kt @@ -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 { override fun get(): JWTAuth { val options = JWTAuthOptions() .addPubSecKey( PubSecKeyOptions() - .setAlgorithm("HS256") - .setBuffer(key) + .setAlgorithm(jwtConfig.algorithm) + .setBuffer(jwtConfig.key) ) return JWTAuth.create(vertx, options) } diff --git a/vertx-demo/src/main/kotlin/app/config/db/DbPoolProvider.kt b/vertx-demo/src/main/kotlin/app/config/db/DbPoolProvider.kt new file mode 100644 index 0000000..4d3942c --- /dev/null +++ b/vertx-demo/src/main/kotlin/app/config/db/DbPoolProvider.kt @@ -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 { + 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 + } +} \ No newline at end of file diff --git a/vertx-demo/src/main/kotlin/app/controller/Demo1Controller.kt b/vertx-demo/src/main/kotlin/app/controller/Demo1Controller.kt index a4f7046..130a750 100644 --- a/vertx-demo/src/main/kotlin/app/controller/Demo1Controller.kt +++ b/vertx-demo/src/main/kotlin/app/controller/Demo1Controller.kt @@ -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) } } diff --git a/vertx-demo/src/main/kotlin/app/port/aipfox/ApifoxClient.kt b/vertx-demo/src/main/kotlin/app/port/aipfox/ApifoxClient.kt index 81b5552..9674ecb 100644 --- a/vertx-demo/src/main/kotlin/app/port/aipfox/ApifoxClient.kt +++ b/vertx-demo/src/main/kotlin/app/port/aipfox/ApifoxClient.kt @@ -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) diff --git a/vertx-demo/src/main/kotlin/app/port/reids/RedisClient.kt b/vertx-demo/src/main/kotlin/app/port/reids/RedisClient.kt index 3f8d1d9..f731328 100644 --- a/vertx-demo/src/main/kotlin/app/port/reids/RedisClient.kt +++ b/vertx-demo/src/main/kotlin/app/port/reids/RedisClient.kt @@ -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毫秒 diff --git a/vertx-demo/src/main/kotlin/app/verticle/WebVerticle.kt b/vertx-demo/src/main/kotlin/app/verticle/WebVerticle.kt index 7035a06..2a928da 100644 --- a/vertx-demo/src/main/kotlin/app/verticle/WebVerticle.kt +++ b/vertx-demo/src/main/kotlin/app/verticle/WebVerticle.kt @@ -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) diff --git a/vertx-demo/src/main/resources/bootstrap.yml b/vertx-demo/src/main/resources/bootstrap.yml index 2ef8b9f..76af595 100644 --- a/vertx-demo/src/main/resources/bootstrap.yml +++ b/vertx-demo/src/main/resources/bootstrap.yml @@ -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 \ No newline at end of file + context: /api +# active: dev + package: app \ No newline at end of file diff --git a/vertx-demo/src/main/resources/config/application.yaml b/vertx-demo/src/main/resources/config/application.yaml new file mode 100644 index 0000000..c54c6f6 --- /dev/null +++ b/vertx-demo/src/main/resources/config/application.yaml @@ -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天 \ No newline at end of file diff --git a/vertx-demo/src/main/resources/logback.xml b/vertx-demo/src/main/resources/logback.xml index 2b9418f..2d900fe 100644 --- a/vertx-demo/src/main/resources/logback.xml +++ b/vertx-demo/src/main/resources/logback.xml @@ -1,7 +1,7 @@ - + diff --git a/vertx-demo/src/test/kotlin/app/GenerateMigration.kt b/vertx-demo/src/test/kotlin/app/GenerateMigration.kt index 49b3538..538d5e9 100644 --- a/vertx-demo/src/test/kotlin/app/GenerateMigration.kt +++ b/vertx-demo/src/test/kotlin/app/GenerateMigration.kt @@ -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( diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/AppConfigs.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/AppConfigs.kt new file mode 100644 index 0000000..9c4054e --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/AppConfigs.kt @@ -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 +) \ No newline at end of file diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/Config.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/Config.kt index e815242..e515982 100644 --- a/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/Config.kt +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/Config.kt @@ -12,7 +12,7 @@ import java.util.concurrent.atomic.AtomicReference object Config { private val retriever = AtomicReference(null) - private var configMap = emptyMap() + private val configMapRef = AtomicReference>(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? { + 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 = emptyList()): List { + return (configMapRef.get()[key] as? JsonArray)?.mapNotNull { it?.toString() } ?: defaultValue + } + + fun getStringListOrNull(key: String): List? { + return (configMapRef.get()[key] as? JsonArray)?.mapNotNull { it?.toString() } } fun getConfigMap(): Map { - return configMap + return configMapRef.get() } private suspend fun load(vertx: Vertx): ConfigRetriever { diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/FrameworkConfigModule.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/FrameworkConfigModule.kt new file mode 100644 index 0000000..6be0a24 --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/FrameworkConfigModule.kt @@ -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) + } +} \ No newline at end of file