vertx-pj:0.0.1

This commit is contained in:
AiKrai 2025-01-10 08:37:58 +08:00
parent f28e1341d0
commit 9f2105caf8
11 changed files with 385 additions and 2 deletions

4
.gitignore vendored
View File

@ -3,8 +3,8 @@ build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
config/
gradle/
/config
/gradle
### IntelliJ IDEA ###
.idea

View File

@ -0,0 +1,7 @@
package app.config
class Constant {
companion object {
const val USER = "user:"
}
}

View File

@ -0,0 +1,93 @@
package app.config
import app.config.auth.JWTAuthProvider
import cn.hutool.core.lang.Snowflake
import cn.hutool.core.util.IdUtil
import com.google.inject.*
import com.google.inject.name.Names
import io.vertx.core.Vertx
import io.vertx.core.http.HttpServer
import io.vertx.core.http.HttpServerOptions
import io.vertx.ext.auth.jwt.JWTAuth
import io.vertx.mysqlclient.MySQLBuilder
import io.vertx.mysqlclient.MySQLConnectOptions
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
object InjectConfig {
suspend fun configure(vertx: Vertx): Injector {
Config.init(vertx)
return Guice.createInjector(InjectorModule(vertx))
}
}
class InjectorModule(
private val vertx: Vertx,
) : AbstractModule() {
override fun configure() {
val pool = getDbPool().also { initTxMgr(it) }
val coroutineScope = DefaultScope(vertx)
for ((key, value) in Config.getConfigMap()) {
bind(String::class.java).annotatedWith(Names.named(key)).toInstance(value.toString())
}
bind(Vertx::class.java).toInstance(vertx)
bind(CoroutineScope::class.java).toInstance(coroutineScope)
bind(HttpServer::class.java).toInstance(vertx.createHttpServer(HttpServerOptions()))
bind(Snowflake::class.java).toInstance(IdUtil.getSnowflake())
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 type = Config.getKey("databases.type").toString()
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 pool = when (type.lowercase()) {
"mysql" -> {
val clientOptions = MySQLConnectOptions()
.setHost(host)
.setPort(port.toInt())
.setDatabase(name)
.setUser(user)
.setPassword(password)
.setTcpKeepAlive(true)
MySQLBuilder.pool().connectingTo(clientOptions).with(poolOptions).using(vertx).build()
}
"postgre", "postgresql" -> {
val clientOptions = PgConnectOptions()
.setHost(host)
.setPort(port.toInt())
.setDatabase(name)
.setUser(user)
.setPassword(password)
.setTcpKeepAlive(true)
PgBuilder.pool().connectingTo(clientOptions).with(poolOptions).using(vertx).build()
}
else -> throw IllegalArgumentException("Unsupported database type: $type")
}
return pool
}
}

View File

@ -0,0 +1,31 @@
package app.config.auth
import app.config.Constant
import app.domain.user.UserRepository
import app.util.CacheUtil
import com.google.inject.Inject
import io.vertx.ext.auth.jwt.JWTAuth
import org.aikrai.vertx.auth.Attributes
import org.aikrai.vertx.auth.AuthUser
import org.aikrai.vertx.auth.Principal
import org.aikrai.vertx.auth.TokenUtil
class AuthHandler @Inject constructor(
private val jwtAuth: JWTAuth,
private val userRepository: UserRepository,
private val cacheUtil: CacheUtil
) {
suspend fun handle(token: String): AuthUser? {
val userInfo = TokenUtil.authenticate(jwtAuth, token) ?: return null
val userId = userInfo.principal().getString("id").toLong()
val user = cacheUtil.get(Constant.USER + userId) ?: userRepository.get(userId)?.let {
cacheUtil.put(Constant.USER + userId, it)
} ?: return null
return AuthUser(
Principal(userId, user),
// get roles and permissions from database
Attributes(setOf("admin"), setOf("user:list")),
)
}
}

View File

@ -0,0 +1,24 @@
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
class JWTAuthProvider @Inject constructor(
private val vertx: Vertx,
@Named("jwt.key") private val key: String
) : Provider<JWTAuth> {
override fun get(): JWTAuth {
val options = JWTAuthOptions()
.addPubSecKey(
PubSecKeyOptions()
.setAlgorithm("HS256")
.setBuffer(key)
)
return JWTAuth.create(vertx, options)
}
}

View File

@ -0,0 +1,56 @@
package app.config.auth
import io.vertx.ext.web.RoutingContext
import io.vertx.ext.web.handler.AuthenticationHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.aikrai.vertx.utlis.Meta
class JwtAuthenticationHandler(
private val coroutineScope: CoroutineScope,
private val authHandler: AuthHandler,
private val context: String,
) : AuthenticationHandler {
var exclude = mutableListOf(
"/auth/**",
)
override fun handle(event: RoutingContext) {
val path = event.request().path().replace("$context/", "/").replace("//", "/")
if (isPathExcluded(path, exclude)) {
event.next()
return
}
val authorization = event.request().getHeader("Authorization") ?: null
if (authorization == null || !authorization.startsWith("token ")) {
event.fail(401, Meta.unauthorized("无效Token"))
return
}
val token = authorization.substring(6)
coroutineScope.launch {
val authUser = authHandler.handle(token)
if (authUser != null) {
event.setUser(authUser)
event.next()
} else {
event.fail(401, Meta.unauthorized("token"))
}
}
}
private fun isPathExcluded(path: String, excludePatterns: List<String>): Boolean {
for (pattern in excludePatterns) {
val regexPattern = pattern
.replace("**", ".+")
.replace("*", "[^/]+")
.replace("?", ".")
val isExclude = path.matches(regexPattern.toRegex())
if (isExclude) return true
}
return false
}
}

View File

@ -0,0 +1,9 @@
databases:
has-open: true
type: mysql
driver-class-name: com.mysql.jdbc.Driver
name: vertx-demo
host: 127.0.0.1
port: 3306
username: root
password: 123456

View File

@ -0,0 +1,9 @@
databases:
has-open: true
type: mysql
driver-class-name: com.mysql.jdbc.Driver
name: vertx-demo
host: 127.0.0.1
port: 3306
username: root
password: 123456

View File

@ -0,0 +1,92 @@
package org.aikrai.vertx.config
import io.vertx.config.ConfigRetriever
import io.vertx.config.ConfigRetrieverOptions
import io.vertx.config.ConfigStoreOptions
import io.vertx.core.Vertx
import io.vertx.core.json.JsonArray
import io.vertx.core.json.JsonObject
import io.vertx.kotlin.coroutines.coAwait
import org.aikrai.vertx.utlis.FlattenUtil
import java.util.concurrent.atomic.AtomicReference
object Config {
private val retriever = AtomicReference<ConfigRetriever?>(null)
private var configMap = emptyMap<String, Any>()
suspend fun init(vertx: Vertx) {
if (retriever.get() != null) return
val configRetriever = load(vertx)
val cas = retriever.compareAndSet(null, configRetriever)
if (cas) {
val configObj = configRetriever.config.coAwait()
configMap = 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 开头的条目
configMap.filterKeys { it.startsWith(key) }
}
}
fun getConfigMap(): Map<String, Any> {
return configMap
}
private suspend fun load(vertx: Vertx): ConfigRetriever {
val sysStore = ConfigStoreOptions().setType("sys")
val envStore = ConfigStoreOptions().setType("env").setConfig(JsonObject().put("raw-data", true))
val bootstrapStore = ConfigStoreOptions().setType("file").setFormat("yaml")
.setConfig(JsonObject().put("path", "bootstrap.yml"))
val bootstrapOptions = ConfigRetrieverOptions()
.addStore(bootstrapStore)
.addStore(sysStore)
.addStore(envStore)
val bootstrapRetriever = ConfigRetriever.create(vertx, bootstrapOptions)
val bootstrapConfig = bootstrapRetriever.config.coAwait()
val useDir = bootstrapConfig.getString("user.dir")
val environment = bootstrapConfig.getJsonObject("server").getString("active")
// 创建资源目录配置存储
val rDirectoryStore = createDirectoryStore("$useDir/src/main/resources/config")
// 创建项目根目录配置存储
val pDirectoryStore = createDirectoryStore(useDir)
// 创建环境相关配置存储
val directoryStore = createDirectoryStore("config${if (!environment.isNullOrBlank()) "/$environment" else ""}")
// 后加载的配置会覆盖前面加载的相同的配置
val options = ConfigRetrieverOptions()
// 项目的resources目录下
.addStore(rDirectoryStore)
// 项目根目录下
.addStore(pDirectoryStore)
// 项目根目录下的config目录
.addStore(directoryStore)
// bootstrap.yml 文件
.addStore(bootstrapStore)
return ConfigRetriever.create(vertx, options)
}
private fun createDirectoryStore(path: String): ConfigStoreOptions {
return ConfigStoreOptions()
.setType("directory")
.setConfig(
JsonObject().put("path", path).put(
"filesets",
JsonArray()
.add(JsonObject().put("pattern", "*.yml").put("format", "yaml"))
.add(JsonObject().put("pattern", "*.yaml").put("format", "yaml"))
.add(JsonObject().put("pattern", "*.properties").put("format", "properties"))
.add(JsonObject().put("pattern", "*.json").put("format", "json"))
)
)
}
}

View File

@ -0,0 +1,15 @@
package org.aikrai.vertx.config
import io.vertx.core.Vertx
import io.vertx.kotlin.coroutines.dispatcher
import kotlinx.coroutines.CoroutineScope
import kotlin.coroutines.CoroutineContext
import kotlin.properties.Delegates
class DefaultScope(private val vertx: Vertx) : CoroutineScope {
override var coroutineContext: CoroutineContext by Delegates.notNull()
init {
coroutineContext = vertx.orCreateContext.dispatcher()
}
}

View File

@ -0,0 +1,47 @@
package org.aikrai.vertx.config
import io.vertx.mysqlclient.MySQLException
import io.vertx.pgclient.PgException
import org.aikrai.vertx.jackson.JsonUtil
import org.aikrai.vertx.utlis.Meta
import java.sql.SQLException
object FailureParser {
private fun Throwable.toMeta(): Meta {
val error = this as? Meta
if (error != null) {
return error
}
val name = javaClass.simpleName
val pgException = this as? PgException
if (pgException != null) {
return Meta(name, pgException.errorMessage ?: "", null)
}
val mysqlException = this as? MySQLException
if (mysqlException != null) {
return Meta(name, mysqlException.message ?: "", null)
}
val message = if (message != null) message.orEmpty() else toString()
return Meta(name, message, null)
}
private fun Throwable.info(): String {
if (this is Meta) {
return JsonUtil.toJsonStr(this)
}
return stackTraceToString()
}
data class Failure(val statusCode: Int, val response: Meta)
fun parse(statusCode: Int, error: Throwable): Failure {
return when (error) {
is SQLException -> Failure(statusCode, Meta.failure(error.javaClass.name, "执行错误"))
else -> Failure(statusCode, error.toMeta())
}
}
}