refactor(vertx-fw):重构异常处理
This commit is contained in:
parent
e7016373c2
commit
394161415c
@ -1,7 +1,7 @@
|
|||||||
package app.config
|
package app.config
|
||||||
|
|
||||||
import app.config.auth.JWTAuthProvider
|
import app.config.provider.JWTAuthProvider
|
||||||
import app.config.db.DbPoolProvider
|
import app.config.provider.DbPoolProvider
|
||||||
import cn.hutool.core.lang.Snowflake
|
import cn.hutool.core.lang.Snowflake
|
||||||
import cn.hutool.core.util.IdUtil
|
import cn.hutool.core.util.IdUtil
|
||||||
import com.google.inject.AbstractModule
|
import com.google.inject.AbstractModule
|
||||||
@ -15,32 +15,40 @@ import io.vertx.sqlclient.SqlClient
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import org.aikrai.vertx.config.DefaultScope
|
import org.aikrai.vertx.config.DefaultScope
|
||||||
import org.aikrai.vertx.config.FrameworkConfigModule
|
import org.aikrai.vertx.config.FrameworkConfigModule
|
||||||
|
import org.aikrai.vertx.http.GlobalErrorHandler
|
||||||
|
import org.aikrai.vertx.http.RequestLogHandler
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 依赖注入配置
|
||||||
|
*/
|
||||||
object InjectConfig {
|
object InjectConfig {
|
||||||
fun configure(vertx: Vertx): Injector {
|
fun configure(vertx: Vertx): Injector {
|
||||||
return Guice.createInjector(InjectorModule(vertx))
|
return Guice.createInjector(InjectorModule(vertx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guice模块配置
|
||||||
|
*/
|
||||||
class InjectorModule(
|
class InjectorModule(
|
||||||
private val vertx: Vertx,
|
private val vertx: Vertx,
|
||||||
) : AbstractModule() {
|
) : AbstractModule() {
|
||||||
override fun configure() {
|
override fun configure() {
|
||||||
// 1. 安装框架提供的配置模块
|
|
||||||
install(FrameworkConfigModule())
|
install(FrameworkConfigModule())
|
||||||
|
|
||||||
// 2. 绑定 Vertx 实例和 CoroutineScope
|
|
||||||
bind(Vertx::class.java).toInstance(vertx)
|
bind(Vertx::class.java).toInstance(vertx)
|
||||||
bind(CoroutineScope::class.java).toInstance(DefaultScope(vertx))
|
bind(CoroutineScope::class.java).toInstance(DefaultScope(vertx))
|
||||||
|
|
||||||
// 3. 绑定 Snowflake
|
|
||||||
bind(Snowflake::class.java).toInstance(IdUtil.getSnowflake())
|
bind(Snowflake::class.java).toInstance(IdUtil.getSnowflake())
|
||||||
|
|
||||||
// 4. 绑定数据库连接池 (使用 Provider 来延迟创建)
|
|
||||||
bind(Pool::class.java).toProvider(DbPoolProvider::class.java).`in`(Singleton::class.java)
|
bind(Pool::class.java).toProvider(DbPoolProvider::class.java).`in`(Singleton::class.java)
|
||||||
bind(SqlClient::class.java).to(Pool::class.java) // 绑定 SqlClient 到 Pool
|
bind(SqlClient::class.java).to(Pool::class.java)
|
||||||
|
|
||||||
// 5. 绑定 JWTAuth
|
// 5. 绑定 JWTAuth
|
||||||
bind(JWTAuth::class.java).toProvider(JWTAuthProvider::class.java).`in`(Singleton::class.java)
|
bind(JWTAuth::class.java).toProvider(JWTAuthProvider::class.java).`in`(Singleton::class.java)
|
||||||
|
|
||||||
|
// 6. 绑定错误处理和日志组件
|
||||||
|
bind(GlobalErrorHandler::class.java).`in`(Singleton::class.java)
|
||||||
|
bind(RequestLogHandler::class.java).`in`(Singleton::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,52 +1,54 @@
|
|||||||
package app.config
|
//package app.config
|
||||||
|
//
|
||||||
import org.aikrai.vertx.constant.HttpStatus
|
//import com.fasterxml.jackson.annotation.JsonInclude
|
||||||
|
//import org.aikrai.vertx.constant.HttpStatus
|
||||||
data class RespBean(
|
//
|
||||||
val code: Int,
|
//@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
val message: String,
|
//data class RespBean(
|
||||||
val data: Any?
|
// val code: Int,
|
||||||
) {
|
// val message: String,
|
||||||
var requestId: Long = -1L
|
// val data: Any?
|
||||||
|
//) {
|
||||||
companion object {
|
// var requestId: Long = -1L
|
||||||
/**
|
//
|
||||||
* 创建一个成功的响应
|
// companion object {
|
||||||
*
|
// /**
|
||||||
* @param data 响应数据
|
// * 创建一个成功的响应
|
||||||
* @return RespBean 实例
|
// *
|
||||||
*/
|
// * @param data 响应数据
|
||||||
fun success(data: Any? = null): RespBean {
|
// * @return RespBean 实例
|
||||||
val code = when (data) {
|
// */
|
||||||
null -> HttpStatus.NO_CONTENT
|
// fun success(data: Any? = null): RespBean {
|
||||||
else -> HttpStatus.SUCCESS
|
// val code = when (data) {
|
||||||
}
|
// null -> HttpStatus.NO_CONTENT
|
||||||
return RespBean(code, "Success", data)
|
// else -> HttpStatus.SUCCESS
|
||||||
}
|
// }
|
||||||
|
// return RespBean(code, "Success", data)
|
||||||
/**
|
// }
|
||||||
* 创建一个失败的响应
|
//
|
||||||
*
|
// /**
|
||||||
* @param status 状态码
|
// * 创建一个失败的响应
|
||||||
* @param message 错误消息
|
// *
|
||||||
* @return RespBean 实例
|
// * @param status 状态码
|
||||||
*/
|
// * @param message 错误消息
|
||||||
fun failure(message: String, data: Any? = null): RespBean {
|
// * @return RespBean 实例
|
||||||
return failure(HttpStatus.ERROR, message, data)
|
// */
|
||||||
}
|
// fun failure(message: String, data: Any? = null): RespBean {
|
||||||
|
// return failure(HttpStatus.ERROR, message, data)
|
||||||
fun failure(code: Int, message: String, data: Any? = null): RespBean {
|
// }
|
||||||
return RespBean(code, message, data)
|
//
|
||||||
}
|
// fun failure(code: Int, message: String, data: Any? = null): RespBean {
|
||||||
|
// return RespBean(code, message, data)
|
||||||
// 访问受限,授权过期
|
// }
|
||||||
fun forbidden(message: String?): RespBean {
|
//
|
||||||
return failure(HttpStatus.FORBIDDEN, message ?: "Restricted access, expired authorizations")
|
// // 访问受限,授权过期
|
||||||
}
|
// fun forbidden(message: String?): RespBean {
|
||||||
|
// return failure(HttpStatus.FORBIDDEN, message ?: "Restricted access, expired authorizations")
|
||||||
// 未授权
|
// }
|
||||||
fun unauthorized(message: String?): RespBean {
|
//
|
||||||
return failure(HttpStatus.UNAUTHORIZED, message ?: "Unauthorized")
|
// // 未授权
|
||||||
}
|
// fun unauthorized(message: String?): RespBean {
|
||||||
}
|
// return failure(HttpStatus.UNAUTHORIZED, message ?: "Unauthorized")
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
package app.config.auth
|
|
||||||
|
|
||||||
import cn.hutool.core.lang.Snowflake
|
|
||||||
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(
|
|
||||||
val scope: CoroutineScope,
|
|
||||||
val tokenService: TokenService,
|
|
||||||
val context: String,
|
|
||||||
val snowflake: Snowflake
|
|
||||||
) : AuthenticationHandler {
|
|
||||||
override fun handle(event: RoutingContext) {
|
|
||||||
event.put("requestId", snowflake.nextId())
|
|
||||||
val path = event.request().path().replace("$context/", "/").replace("//", "/")
|
|
||||||
if (isPathExcluded(path, anonymous)) {
|
|
||||||
event.next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
val user = tokenService.getLoginUser(event)
|
|
||||||
tokenService.verifyToken(user)
|
|
||||||
event.setUser(user)
|
|
||||||
event.next()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
event.fail(401, Meta.unauthorized(e.message ?: "token"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var anonymous = mutableListOf(
|
|
||||||
"/apidoc.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
package app.config.auth
|
|
||||||
|
|
||||||
import app.config.RespBean
|
|
||||||
import com.google.inject.Singleton
|
|
||||||
import io.vertx.ext.web.RoutingContext
|
|
||||||
import mu.KotlinLogging
|
|
||||||
import org.aikrai.vertx.config.resp.ResponseHandlerInterface
|
|
||||||
import org.aikrai.vertx.constant.HttpStatus
|
|
||||||
import org.aikrai.vertx.jackson.JsonUtil
|
|
||||||
import org.aikrai.vertx.utlis.Meta
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class ResponseHandler : ResponseHandlerInterface {
|
|
||||||
private val logger = KotlinLogging.logger { }
|
|
||||||
|
|
||||||
override suspend fun normal(
|
|
||||||
ctx: RoutingContext,
|
|
||||||
responseData: Any?,
|
|
||||||
customizeResponse: Boolean
|
|
||||||
) {
|
|
||||||
val requestId = ctx.get<Long>("requestId") ?: -1L
|
|
||||||
val code: Int
|
|
||||||
val resStr = when (responseData) {
|
|
||||||
is RespBean -> {
|
|
||||||
code = responseData.code
|
|
||||||
responseData.requestId = requestId
|
|
||||||
JsonUtil.toJsonStr(responseData)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
val respBean = RespBean.success(responseData).apply {
|
|
||||||
this.requestId = requestId
|
|
||||||
}
|
|
||||||
code = respBean.code
|
|
||||||
JsonUtil.toJsonStr(respBean)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.put("responseData", resStr)
|
|
||||||
if (customizeResponse) return
|
|
||||||
ctx.response()
|
|
||||||
.setStatusCode(code)
|
|
||||||
.putHeader("Content-Type", "application/json")
|
|
||||||
.end(resStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 业务异常处理
|
|
||||||
override suspend fun exception(ctx: RoutingContext, e: Throwable) {
|
|
||||||
logger.error { "${ctx.request().uri()}: ${e.stackTraceToString()}" }
|
|
||||||
val resObj = when (e) {
|
|
||||||
is Meta -> {
|
|
||||||
RespBean.failure("${e.name}:${e.message}", e.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
RespBean.failure("${e.javaClass.simpleName}${if (e.message != null) ":${e.message}" else ""}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val resStr = JsonUtil.toJsonStr(resObj)
|
|
||||||
ctx.put("responseData", resStr)
|
|
||||||
ctx.response()
|
|
||||||
.setStatusCode(HttpStatus.ERROR)
|
|
||||||
.putHeader("Content-Type", "application/json")
|
|
||||||
.end(resStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
package app.config.handler
|
||||||
|
|
||||||
|
import app.service.auth.TokenService
|
||||||
|
import com.google.inject.Inject
|
||||||
|
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.config.ServerConfig
|
||||||
|
import org.aikrai.vertx.constant.HttpStatus
|
||||||
|
import org.aikrai.vertx.utlis.Meta
|
||||||
|
import org.slf4j.MDC
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT认证处理器
|
||||||
|
*/
|
||||||
|
class JwtAuthHandler @Inject constructor(
|
||||||
|
val scope: CoroutineScope,
|
||||||
|
val tokenService: TokenService,
|
||||||
|
val serverConfig: ServerConfig,
|
||||||
|
) : AuthenticationHandler {
|
||||||
|
override fun handle(ctx: RoutingContext) {
|
||||||
|
val path = ctx.request().path().replace("${serverConfig.context}/", "/").replace("//", "/")
|
||||||
|
if (isPathExcluded(path, anonymous)) {
|
||||||
|
ctx.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val user = tokenService.getLoginUser(ctx)
|
||||||
|
ctx.setUser(user)
|
||||||
|
|
||||||
|
// 将用户ID放入MDC
|
||||||
|
user.principal().getString("sub")?.let { userId ->
|
||||||
|
MDC.put("userId", userId)
|
||||||
|
}
|
||||||
|
ctx.next()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
MDC.remove("userId")
|
||||||
|
|
||||||
|
val metaError = when (e) {
|
||||||
|
is Meta -> e
|
||||||
|
else -> Meta.unauthorized(e.message ?: "认证失败")
|
||||||
|
}
|
||||||
|
ctx.fail(HttpStatus.UNAUTHORIZED, metaError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var anonymous = mutableListOf(
|
||||||
|
"/apidoc.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查路径是否在排除列表中
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
package app.config.handler
|
||||||
|
|
||||||
|
import com.google.inject.Singleton
|
||||||
|
import io.vertx.core.http.HttpHeaders
|
||||||
|
import io.vertx.ext.web.RoutingContext
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.aikrai.vertx.http.RespBean
|
||||||
|
import org.aikrai.vertx.jackson.JsonUtil
|
||||||
|
import org.aikrai.vertx.resp.ResponseHandlerInterface
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应处理器,负责处理API响应
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class ResponseHandler : ResponseHandlerInterface {
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理成功响应
|
||||||
|
*/
|
||||||
|
override suspend fun handle(
|
||||||
|
ctx: RoutingContext,
|
||||||
|
responseData: Any?,
|
||||||
|
customizeResponse: Boolean
|
||||||
|
) {
|
||||||
|
// 使用RequestLogHandler设置的请求ID
|
||||||
|
val requestId = ctx.get<String>("requestId")
|
||||||
|
val code: Int
|
||||||
|
val resStr = when (responseData) {
|
||||||
|
is RespBean<*> -> {
|
||||||
|
code = responseData.code
|
||||||
|
responseData.requestId = requestId
|
||||||
|
JsonUtil.toJsonStr(responseData)
|
||||||
|
}
|
||||||
|
// 否则使用RespBean包装
|
||||||
|
else -> {
|
||||||
|
val respBean = RespBean.Companion.success(responseData)
|
||||||
|
respBean.requestId = requestId
|
||||||
|
code = respBean.code
|
||||||
|
JsonUtil.toJsonStr(respBean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.put("responseData", resStr) // 存储响应内容用于日志
|
||||||
|
|
||||||
|
if (customizeResponse) return // 如果需要自定义响应,则不发送标准响应
|
||||||
|
|
||||||
|
ctx.response()
|
||||||
|
.setStatusCode(code)
|
||||||
|
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
|
||||||
|
.end(resStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package app.config.db
|
package app.config.provider
|
||||||
|
|
||||||
import com.google.inject.Inject
|
import com.google.inject.Inject
|
||||||
import com.google.inject.Provider
|
import com.google.inject.Provider
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package app.config.auth
|
package app.config.provider
|
||||||
|
|
||||||
import com.google.inject.Inject
|
import com.google.inject.Inject
|
||||||
import com.google.inject.Provider
|
import com.google.inject.Provider
|
||||||
@ -10,6 +10,7 @@ import org.aikrai.vertx.auth.AllowAnonymous
|
|||||||
import org.aikrai.vertx.config.Config
|
import org.aikrai.vertx.config.Config
|
||||||
import org.aikrai.vertx.context.Controller
|
import org.aikrai.vertx.context.Controller
|
||||||
import org.aikrai.vertx.context.D
|
import org.aikrai.vertx.context.D
|
||||||
|
import org.aikrai.vertx.utlis.Meta
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 推荐代码示例
|
* 推荐代码示例
|
||||||
@ -32,6 +33,7 @@ class Demo1Controller @Inject constructor(
|
|||||||
@D("account", "账号") account: Account?
|
@D("account", "账号") account: Account?
|
||||||
) {
|
) {
|
||||||
logger.info { "你好" }
|
logger.info { "你好" }
|
||||||
|
throw Meta.error("test", "test")
|
||||||
println(age)
|
println(age)
|
||||||
println(list)
|
println(list)
|
||||||
println("test-$name")
|
println("test-$name")
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
package app.service.account
|
package app.service.account
|
||||||
|
|
||||||
import app.config.auth.TokenService
|
|
||||||
import app.data.domain.account.Account
|
import app.data.domain.account.Account
|
||||||
import app.data.domain.account.AccountRepository
|
import app.data.domain.account.AccountRepository
|
||||||
import app.data.domain.account.LoginDTO
|
import app.data.domain.account.LoginDTO
|
||||||
|
import app.service.auth.TokenService
|
||||||
import cn.hutool.core.lang.Snowflake
|
import cn.hutool.core.lang.Snowflake
|
||||||
import cn.hutool.crypto.SecureUtil
|
import cn.hutool.crypto.SecureUtil
|
||||||
import com.google.inject.Inject
|
import com.google.inject.Inject
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
package app.config.auth
|
package app.service.auth
|
||||||
|
|
||||||
import app.data.domain.account.AccountRepository
|
import app.data.domain.account.AccountRepository
|
||||||
import app.port.reids.RedisClient
|
import app.port.reids.RedisClient
|
||||||
import cn.hutool.core.lang.Snowflake
|
|
||||||
import cn.hutool.core.util.IdUtil
|
import cn.hutool.core.util.IdUtil
|
||||||
import com.google.inject.Inject
|
import com.google.inject.Inject
|
||||||
import com.google.inject.Singleton
|
import com.google.inject.Singleton
|
||||||
@ -23,7 +22,6 @@ import org.aikrai.vertx.utlis.Meta
|
|||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class TokenService @Inject constructor(
|
class TokenService @Inject constructor(
|
||||||
private val snowflake: Snowflake,
|
|
||||||
private val jwtAuth: JWTAuth,
|
private val jwtAuth: JWTAuth,
|
||||||
private val redisClient: RedisClient,
|
private val redisClient: RedisClient,
|
||||||
private val accountRepository: AccountRepository,
|
private val accountRepository: AccountRepository,
|
||||||
@ -54,8 +52,6 @@ class TokenService @Inject constructor(
|
|||||||
return genToken(mapOf(Constants.LOGIN_USER_KEY to token))
|
return genToken(mapOf(Constants.LOGIN_USER_KEY to token))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun verifyToken(loginUser: AuthUser) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun genToken(info: Map<String, Any>, expires: Int? = null): String {
|
private fun genToken(info: Map<String, Any>, expires: Int? = null): String {
|
||||||
val jwtOptions = JWTOptions().setExpiresInSeconds(expires ?: (60 * 60 * 24 * 7))
|
val jwtOptions = JWTOptions().setExpiresInSeconds(expires ?: (60 * 60 * 24 * 7))
|
||||||
@ -1,57 +1,49 @@
|
|||||||
package app.verticle
|
package app.verticle
|
||||||
|
|
||||||
import app.config.RespBean
|
import app.config.handler.JwtAuthHandler
|
||||||
import app.config.auth.JwtAuthenticationHandler
|
import app.config.handler.ResponseHandler
|
||||||
import app.config.auth.ResponseHandler
|
|
||||||
import app.config.auth.TokenService
|
|
||||||
import app.data.domain.account.Account
|
|
||||||
import app.port.aipfox.ApifoxClient
|
import app.port.aipfox.ApifoxClient
|
||||||
import cn.hutool.core.lang.Snowflake
|
|
||||||
import com.google.inject.Inject
|
import com.google.inject.Inject
|
||||||
import com.google.inject.Injector
|
import com.google.inject.Injector
|
||||||
import io.vertx.core.Handler
|
|
||||||
import io.vertx.core.http.HttpHeaders
|
|
||||||
import io.vertx.core.http.HttpMethod
|
import io.vertx.core.http.HttpMethod
|
||||||
import io.vertx.core.http.HttpServerOptions
|
import io.vertx.core.http.HttpServerOptions
|
||||||
import io.vertx.ext.web.Router
|
import io.vertx.ext.web.Router
|
||||||
import io.vertx.ext.web.RoutingContext
|
|
||||||
import io.vertx.ext.web.handler.BodyHandler
|
import io.vertx.ext.web.handler.BodyHandler
|
||||||
import io.vertx.ext.web.handler.CorsHandler
|
import io.vertx.ext.web.handler.CorsHandler
|
||||||
import io.vertx.kotlin.coroutines.CoroutineVerticle
|
import io.vertx.kotlin.coroutines.CoroutineVerticle
|
||||||
import io.vertx.kotlin.coroutines.coAwait
|
import io.vertx.kotlin.coroutines.coAwait
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.aikrai.vertx.auth.AuthUser
|
|
||||||
import org.aikrai.vertx.config.ServerConfig
|
import org.aikrai.vertx.config.ServerConfig
|
||||||
import org.aikrai.vertx.context.RouterBuilder
|
import org.aikrai.vertx.context.RouterBuilder
|
||||||
import org.aikrai.vertx.jackson.JsonUtil
|
import org.aikrai.vertx.http.GlobalErrorHandler
|
||||||
import org.aikrai.vertx.utlis.LangUtil.toStringMap
|
import org.aikrai.vertx.http.RequestLogHandler
|
||||||
import org.aikrai.vertx.utlis.Meta
|
|
||||||
|
|
||||||
class WebVerticle @Inject constructor(
|
class WebVerticle @Inject constructor(
|
||||||
private val getIt: Injector,
|
private val getIt: Injector,
|
||||||
|
private val serverConfig: ServerConfig,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val tokenService: TokenService,
|
private val jwtAuthHandler: JwtAuthHandler,
|
||||||
private val apifoxClient: ApifoxClient,
|
private val requestLogHandler: RequestLogHandler,
|
||||||
private val snowflake: Snowflake,
|
|
||||||
private val responseHandler: ResponseHandler,
|
private val responseHandler: ResponseHandler,
|
||||||
private val serverConfig: ServerConfig
|
private val globalErrorHandler: GlobalErrorHandler,
|
||||||
) : CoroutineVerticle() {
|
private val apiFoxClient: ApifoxClient,
|
||||||
|
) : CoroutineVerticle() {
|
||||||
private val logger = KotlinLogging.logger { }
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
override suspend fun start() {
|
override suspend fun start() {
|
||||||
val rootRouter = Router.router(vertx)
|
val rootRouter = Router.router(vertx)
|
||||||
val router = Router.router(vertx)
|
val router = Router.router(vertx)
|
||||||
setupRouter(rootRouter, router)
|
setupRouter(rootRouter, router)
|
||||||
val options = HttpServerOptions().setMaxFormAttributeSize(1024 * 1024)
|
val options = HttpServerOptions()
|
||||||
|
.setMaxFormAttributeSize(1024 * 1024)
|
||||||
val server = vertx.createHttpServer(options)
|
val server = vertx.createHttpServer(options)
|
||||||
.requestHandler(rootRouter)
|
.requestHandler(rootRouter)
|
||||||
.listen(serverConfig.port)
|
.listen(serverConfig.port)
|
||||||
.coAwait()
|
.coAwait()
|
||||||
|
// 生成ApiFox接口
|
||||||
apifoxClient.importOpenapi()
|
apiFoxClient.importOpenapi()
|
||||||
|
logger.info { "HTTP服务启动 - http://127.0.0.1:${server.actualPort()}${serverConfig.context}" }
|
||||||
logger.info { "http server start - http://127.0.0.1:${server.actualPort()}${serverConfig.context}" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun stop() {
|
override suspend fun stop() {
|
||||||
@ -59,19 +51,18 @@ class WebVerticle @Inject constructor(
|
|||||||
|
|
||||||
private fun setupRouter(rootRouter: Router, router: Router) {
|
private fun setupRouter(rootRouter: Router, router: Router) {
|
||||||
rootRouter.route("${serverConfig.context}*").subRouter(router)
|
rootRouter.route("${serverConfig.context}*").subRouter(router)
|
||||||
|
|
||||||
router.route()
|
router.route()
|
||||||
.handler(corsHandler)
|
.handler(corsHandler)
|
||||||
.handler(BodyHandler.create())
|
.handler(BodyHandler.create())
|
||||||
.handler(logHandler)
|
.handler(jwtAuthHandler)
|
||||||
.failureHandler(errorHandler)
|
.handler(requestLogHandler)
|
||||||
|
.failureHandler(globalErrorHandler)
|
||||||
|
|
||||||
val authHandler = JwtAuthenticationHandler(coroutineScope, tokenService, serverConfig.context, snowflake)
|
val routerBuilder = RouterBuilder(coroutineScope, router, serverConfig.scanPackage, responseHandler)
|
||||||
router.route("/*").handler(authHandler)
|
.build{ getIt.getInstance(it) }
|
||||||
|
|
||||||
val routerBuilder = RouterBuilder(coroutineScope, router, serverConfig.scanPackage, responseHandler).build { service ->
|
jwtAuthHandler.anonymous.addAll(routerBuilder.anonymousPaths)
|
||||||
getIt.getInstance(service)
|
|
||||||
}
|
|
||||||
authHandler.anonymous.addAll(routerBuilder.anonymousPaths)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val corsHandler = CorsHandler.create()
|
private val corsHandler = CorsHandler.create()
|
||||||
@ -81,76 +72,4 @@ class WebVerticle @Inject constructor(
|
|||||||
.allowedMethod(HttpMethod.PUT)
|
.allowedMethod(HttpMethod.PUT)
|
||||||
.allowedMethod(HttpMethod.DELETE)
|
.allowedMethod(HttpMethod.DELETE)
|
||||||
.allowedMethod(HttpMethod.OPTIONS)
|
.allowedMethod(HttpMethod.OPTIONS)
|
||||||
|
|
||||||
// 非业务异常处理
|
|
||||||
private val errorHandler = Handler<RoutingContext> { ctx ->
|
|
||||||
val failure = ctx.failure()
|
|
||||||
if (failure != null) {
|
|
||||||
logger.error { "${ctx.request().uri()}: ${failure.stackTraceToString()}" }
|
|
||||||
val resObj = when (failure) {
|
|
||||||
is Meta -> RespBean.failure(ctx.statusCode(), "${failure.name}:${failure.message}", failure.data)
|
|
||||||
else -> RespBean.failure("${failure.javaClass.simpleName}${if (failure.message != null) ":${failure.message}" else ""}")
|
|
||||||
}
|
|
||||||
val resStr = JsonUtil.toJsonStr(resObj)
|
|
||||||
ctx.put("responseData", resStr)
|
|
||||||
ctx.response()
|
|
||||||
.setStatusCode(ctx.statusCode())
|
|
||||||
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
|
|
||||||
.end(resStr)
|
|
||||||
} else {
|
|
||||||
logger.error("${ctx.request().uri()}: 未知错误")
|
|
||||||
val resObj = RespBean.failure("未知错误")
|
|
||||||
val resStr = JsonUtil.toJsonStr(resObj)
|
|
||||||
ctx.put("responseData", resStr)
|
|
||||||
ctx.response()
|
|
||||||
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
|
|
||||||
.setStatusCode(500)
|
|
||||||
.end(resStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val logHandler = Handler<RoutingContext> { ctx ->
|
|
||||||
val start = System.currentTimeMillis()
|
|
||||||
ctx.response().endHandler {
|
|
||||||
val end = System.currentTimeMillis()
|
|
||||||
val timeCost = "${end - start}ms".let {
|
|
||||||
when (end - start) {
|
|
||||||
in 0..500 -> it
|
|
||||||
in 501..2000 -> "$it⚠️"
|
|
||||||
else -> "$it❌"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val authUser = ctx.user() as? AuthUser
|
|
||||||
val logContent = if (authUser != null) {
|
|
||||||
val user = JsonUtil.parseObject(authUser.user, Account::class.java)
|
|
||||||
"""
|
|
||||||
|
|
|
||||||
|>>>>>请求ID:[${ctx.get<String>("requestId")}]
|
|
||||||
|>>>>>请求URL:[${ctx.request().path()}](${ctx.request().method()})
|
|
||||||
|>>>>>请求IP:[${ctx.request().remoteAddress().host()}]
|
|
||||||
|>>>>>用户名:[${user.userName}]
|
|
||||||
|>>>>>用户ID:[${user.userId}]
|
|
||||||
|>>>>>角色:[${authUser.roles}]
|
|
||||||
|>>>>>请求参数:[${JsonUtil.toJsonStr(ctx.request().params().toStringMap())}]
|
|
||||||
|>>>>>请求体:[${JsonUtil.toJsonStr(ctx.body().asString())}]
|
|
||||||
|>>>>>响应结果:[${ctx.get<String>("responseData")}]
|
|
||||||
|>>>>>耗时:[$timeCost]
|
|
||||||
""".trimMargin()
|
|
||||||
} else {
|
|
||||||
"""
|
|
||||||
|
|
|
||||||
|>>>>>请求ID:[${ctx.get<String>("requestId")}]
|
|
||||||
|>>>>>请求URL:["${ctx.request().uri()}"](${ctx.request().method()})
|
|
||||||
|>>>>>请求IP:[${ctx.request().remoteAddress().host()}]
|
|
||||||
|>>>>>身份:[未验证]
|
|
||||||
|>>>>>请求参数:[${JsonUtil.toJsonStr(ctx.request().params().toStringMap())}]
|
|
||||||
|>>>>>请求体:[${JsonUtil.toJsonStr(ctx.body().asString())}]
|
|
||||||
|>>>>>响应结果:[${ctx.get<String>("responseData")}]
|
|
||||||
|>>>>>耗时:[$timeCost]
|
|
||||||
""".trimMargin()
|
|
||||||
}
|
|
||||||
logger.info(logContent)
|
|
||||||
}
|
|
||||||
ctx.next()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,10 @@
|
|||||||
<property name="FILESIZE" value="500MB"/>
|
<property name="FILESIZE" value="500MB"/>
|
||||||
<property name="MAXHISTORY" value="100"/>
|
<property name="MAXHISTORY" value="100"/>
|
||||||
|
|
||||||
|
<!-- 定义MDC变量,如果不存在则为空字符串 -->
|
||||||
|
<property name="mdcPattern" value="requestId=%X{requestId:-N/A} userId=%X{userId:-anon} ip=%X{remoteAddr:-} method=%X{method:-} path=%X{path:-} status=%X{statusCode:-} time=%X{duration:-}ms" />
|
||||||
|
|
||||||
|
<!-- 控制台输出 -->
|
||||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
<Target>System.out</Target>
|
<Target>System.out</Target>
|
||||||
<encoder charset="utf-8">
|
<encoder charset="utf-8">
|
||||||
@ -14,15 +18,14 @@
|
|||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
|
<!-- 警告级别日志 -->
|
||||||
<appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
<appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${ROOT}${APPNAME}-warn.log</file>
|
||||||
<encoder charset="utf-8">
|
<encoder charset="utf-8">
|
||||||
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
<pattern>[%-5level] %d{ISO8601} [%thread] %logger{36} [${mdcPattern}] - %m%n</pattern>
|
||||||
</pattern>
|
|
||||||
</encoder>
|
</encoder>
|
||||||
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
<evaluator>
|
<level>WARN</level>
|
||||||
<expression>return level >= WARN;</expression>
|
|
||||||
</evaluator>
|
|
||||||
<onMatch>ACCEPT</onMatch>
|
<onMatch>ACCEPT</onMatch>
|
||||||
<onMismatch>DENY</onMismatch>
|
<onMismatch>DENY</onMismatch>
|
||||||
</filter>
|
</filter>
|
||||||
@ -36,15 +39,14 @@
|
|||||||
</rollingPolicy>
|
</rollingPolicy>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
|
<!-- 信息级别日志 -->
|
||||||
<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${ROOT}${APPNAME}-info.log</file>
|
||||||
<encoder charset="utf-8">
|
<encoder charset="utf-8">
|
||||||
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
<pattern>[%-5level] %d{ISO8601} [%thread] %logger{36} [${mdcPattern}] - %m%n</pattern>
|
||||||
</pattern>
|
|
||||||
</encoder>
|
</encoder>
|
||||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
<evaluator>
|
<level>INFO</level>
|
||||||
<expression>return level >= INFO;</expression>
|
|
||||||
</evaluator>
|
|
||||||
<onMatch>ACCEPT</onMatch>
|
<onMatch>ACCEPT</onMatch>
|
||||||
<onMismatch>DENY</onMismatch>
|
<onMismatch>DENY</onMismatch>
|
||||||
</filter>
|
</filter>
|
||||||
@ -58,21 +60,21 @@
|
|||||||
</rollingPolicy>
|
</rollingPolicy>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
|
<!-- 调试级别日志 -->
|
||||||
<appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
<appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${ROOT}${APPNAME}-debug.log</file>
|
||||||
<encoder charset="utf-8">
|
<encoder charset="utf-8">
|
||||||
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
||||||
</pattern>
|
</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
<evaluator>
|
<level>DEBUG</level>
|
||||||
<expression>return level >= DEBUG;</expression>
|
|
||||||
</evaluator>
|
|
||||||
<onMatch>ACCEPT</onMatch>
|
<onMatch>ACCEPT</onMatch>
|
||||||
<onMismatch>DENY</onMismatch>
|
<onMismatch>DENY</onMismatch>
|
||||||
</filter>
|
</filter>
|
||||||
<rollingPolicy
|
<rollingPolicy
|
||||||
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
<fileNamePattern>${ROOT}${APPNAME}-%d-info.%i.log</fileNamePattern>
|
<fileNamePattern>${ROOT}${APPNAME}-%d-debug.%i.log</fileNamePattern>
|
||||||
<maxHistory>${MAXHISTORY}</maxHistory>
|
<maxHistory>${MAXHISTORY}</maxHistory>
|
||||||
<timeBasedFileNamingAndTriggeringPolicy
|
<timeBasedFileNamingAndTriggeringPolicy
|
||||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||||
@ -81,21 +83,21 @@
|
|||||||
</rollingPolicy>
|
</rollingPolicy>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
|
<!-- 跟踪级别日志 -->
|
||||||
<appender name="TRACE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
<appender name="TRACE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${ROOT}${APPNAME}-trace.log</file>
|
||||||
<encoder charset="utf-8">
|
<encoder charset="utf-8">
|
||||||
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
||||||
</pattern>
|
</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
<evaluator>
|
<level>TRACE</level>
|
||||||
<expression>return level >= TRACE;</expression>
|
|
||||||
</evaluator>
|
|
||||||
<onMatch>ACCEPT</onMatch>
|
<onMatch>ACCEPT</onMatch>
|
||||||
<onMismatch>DENY</onMismatch>
|
<onMismatch>DENY</onMismatch>
|
||||||
</filter>
|
</filter>
|
||||||
<rollingPolicy
|
<rollingPolicy
|
||||||
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
<fileNamePattern>${ROOT}${APPNAME}-%d-info.%i.log</fileNamePattern>
|
<fileNamePattern>${ROOT}${APPNAME}-%d-trace.%i.log</fileNamePattern>
|
||||||
<maxHistory>${MAXHISTORY}</maxHistory>
|
<maxHistory>${MAXHISTORY}</maxHistory>
|
||||||
<timeBasedFileNamingAndTriggeringPolicy
|
<timeBasedFileNamingAndTriggeringPolicy
|
||||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||||
@ -104,11 +106,67 @@
|
|||||||
</rollingPolicy>
|
</rollingPolicy>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<root level="DEBUG">
|
<!-- JSON格式日志(所有级别) -->
|
||||||
|
<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${ROOT}${APPNAME}-json.log</file>
|
||||||
|
<!-- 使用 logstash-logback-encoder,需要添加相应依赖 -->
|
||||||
|
<!-- 如果不想使用 logstash-logback-encoder,可以注释掉这个appender或者使用自定义JSON格式 -->
|
||||||
|
<!--
|
||||||
|
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
|
||||||
|
<includeMdc>true</includeMdc>
|
||||||
|
<customFields>{"application":"${APPNAME}"}</customFields>
|
||||||
|
</encoder>
|
||||||
|
-->
|
||||||
|
<!-- 简单的JSON格式输出,不依赖额外库 -->
|
||||||
|
<encoder charset="utf-8">
|
||||||
|
<pattern>{"time":"%d{ISO8601}","level":"%level","thread":"%thread","logger":"%logger","mdc":{%mdc},"message":"%message"}%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${ROOT}${APPNAME}-%d-json.%i.log</fileNamePattern>
|
||||||
|
<maxHistory>${MAXHISTORY}</maxHistory>
|
||||||
|
<timeBasedFileNamingAndTriggeringPolicy
|
||||||
|
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||||
|
<maxFileSize>${FILESIZE}</maxFileSize>
|
||||||
|
</timeBasedFileNamingAndTriggeringPolicy>
|
||||||
|
</rollingPolicy>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 错误级别日志 -->
|
||||||
|
<appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<!-- 添加缺少的file属性 -->
|
||||||
|
<file>${ROOT}${APPNAME}-error.log</file>
|
||||||
|
<encoder charset="utf-8">
|
||||||
|
<pattern>[%-5level] %d{ISO8601} [%thread] %logger{36} [${mdcPattern}] - %m%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
|
<level>ERROR</level>
|
||||||
|
<onMatch>ACCEPT</onMatch>
|
||||||
|
<onMismatch>DENY</onMismatch>
|
||||||
|
</filter>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${ROOT}${APPNAME}-%d-error.%i.log</fileNamePattern>
|
||||||
|
<maxHistory>${MAXHISTORY}</maxHistory>
|
||||||
|
<timeBasedFileNamingAndTriggeringPolicy
|
||||||
|
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||||
|
<maxFileSize>${FILESIZE}</maxFileSize>
|
||||||
|
</timeBasedFileNamingAndTriggeringPolicy>
|
||||||
|
</rollingPolicy>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 根Logger配置 -->
|
||||||
|
<root level="INFO">
|
||||||
<appender-ref ref="STDOUT"/>
|
<appender-ref ref="STDOUT"/>
|
||||||
|
<appender-ref ref="ERROR"/>
|
||||||
<appender-ref ref="WARN"/>
|
<appender-ref ref="WARN"/>
|
||||||
<appender-ref ref="INFO"/>
|
<appender-ref ref="INFO"/>
|
||||||
<appender-ref ref="DEBUG"/>
|
<appender-ref ref="DEBUG"/>
|
||||||
<appender-ref ref="TRACE"/>
|
<appender-ref ref="TRACE"/>
|
||||||
|
<appender-ref ref="JSON_FILE"/>
|
||||||
</root>
|
</root>
|
||||||
|
|
||||||
|
<!-- 可选:为特定包设置日志级别 -->
|
||||||
|
<!--
|
||||||
|
<logger name="app" level="DEBUG"/>
|
||||||
|
<logger name="org.aikrai.vertx" level="DEBUG"/>
|
||||||
|
-->
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
@ -72,7 +72,7 @@ object Config {
|
|||||||
val map = configMapRef.get()
|
val map = configMapRef.get()
|
||||||
val subMap = map.filterKeys { it.startsWith("$keyPrefix.") }
|
val subMap = map.filterKeys { it.startsWith("$keyPrefix.") }
|
||||||
.mapKeys { it.key.removePrefix("$keyPrefix.") }
|
.mapKeys { it.key.removePrefix("$keyPrefix.") }
|
||||||
return if (subMap.isEmpty()) null else subMap
|
return subMap.ifEmpty { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getStringList(key: String, defaultValue: List<String> = emptyList()): List<String> {
|
fun getStringList(key: String, defaultValue: List<String> = emptyList()): List<String> {
|
||||||
|
|||||||
@ -11,10 +11,6 @@ import com.google.inject.Singleton
|
|||||||
*/
|
*/
|
||||||
class FrameworkConfigModule : AbstractModule() {
|
class FrameworkConfigModule : AbstractModule() {
|
||||||
|
|
||||||
override fun configure() {
|
|
||||||
// 这里不需要bind(Config::class.java),因为Config是object
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideDatabaseConfig(): DatabaseConfig {
|
fun provideDatabaseConfig(): DatabaseConfig {
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
package org.aikrai.vertx.config.resp
|
|
||||||
|
|
||||||
import io.vertx.core.http.HttpHeaders
|
|
||||||
import io.vertx.ext.web.RoutingContext
|
|
||||||
import mu.KotlinLogging
|
|
||||||
import org.aikrai.vertx.constant.HttpStatus
|
|
||||||
import org.aikrai.vertx.jackson.JsonUtil
|
|
||||||
|
|
||||||
class DefaultResponseHandler: ResponseHandlerInterface {
|
|
||||||
private val logger = KotlinLogging.logger { }
|
|
||||||
|
|
||||||
override suspend fun normal(
|
|
||||||
ctx: RoutingContext,
|
|
||||||
responseData: Any?,
|
|
||||||
customizeResponse: Boolean
|
|
||||||
) {
|
|
||||||
val resStr = JsonUtil.toJsonStr(responseData)
|
|
||||||
ctx.put("responseData", resStr)
|
|
||||||
if (customizeResponse) return
|
|
||||||
ctx.response()
|
|
||||||
.setStatusCode(HttpStatus.SUCCESS)
|
|
||||||
.putHeader("Content-Type", "application/json")
|
|
||||||
.end(resStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun exception(ctx: RoutingContext, e: Throwable) {
|
|
||||||
logger.error { "${ctx.request().uri()}: ${ctx.failure().stackTraceToString()}" }
|
|
||||||
val failure = ctx.failure()
|
|
||||||
if (failure == null) {
|
|
||||||
ctx.response()
|
|
||||||
.setStatusCode(ctx.statusCode())
|
|
||||||
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
|
|
||||||
.end()
|
|
||||||
} else {
|
|
||||||
val resStr = JsonUtil.toJsonStr(failure)
|
|
||||||
ctx.put("responseData", resStr)
|
|
||||||
ctx.response()
|
|
||||||
.setStatusCode(ctx.statusCode())
|
|
||||||
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
|
|
||||||
.end(resStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
package org.aikrai.vertx.config.resp
|
|
||||||
|
|
||||||
import io.vertx.ext.web.RoutingContext
|
|
||||||
|
|
||||||
interface ResponseHandlerInterface {
|
|
||||||
suspend fun normal(ctx: RoutingContext, responseData: Any?, customizeResponse: Boolean = false)
|
|
||||||
suspend fun exception(ctx: RoutingContext, e: Throwable)
|
|
||||||
}
|
|
||||||
@ -10,10 +10,10 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.aikrai.vertx.auth.*
|
import org.aikrai.vertx.auth.*
|
||||||
import org.aikrai.vertx.auth.AuthUser.Companion.validateAuth
|
import org.aikrai.vertx.auth.AuthUser.Companion.validateAuth
|
||||||
import org.aikrai.vertx.config.resp.DefaultResponseHandler
|
|
||||||
import org.aikrai.vertx.config.resp.ResponseHandlerInterface
|
|
||||||
import org.aikrai.vertx.db.annotation.EnumValue
|
import org.aikrai.vertx.db.annotation.EnumValue
|
||||||
import org.aikrai.vertx.jackson.JsonUtil
|
import org.aikrai.vertx.jackson.JsonUtil
|
||||||
|
import org.aikrai.vertx.resp.DefaultResponseHandler
|
||||||
|
import org.aikrai.vertx.resp.ResponseHandlerInterface
|
||||||
import org.aikrai.vertx.utlis.ClassUtil
|
import org.aikrai.vertx.utlis.ClassUtil
|
||||||
import org.aikrai.vertx.utlis.Meta
|
import org.aikrai.vertx.utlis.Meta
|
||||||
import org.reflections.Reflections
|
import org.reflections.Reflections
|
||||||
@ -214,9 +214,10 @@ class RouterBuilder(
|
|||||||
} else {
|
} else {
|
||||||
routeInfo.kFunction.call(instance, *params)
|
routeInfo.kFunction.call(instance, *params)
|
||||||
}
|
}
|
||||||
responseHandler.normal(ctx, result, routeInfo.customizeResp)
|
responseHandler.handle(ctx, result, routeInfo.customizeResp)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
responseHandler.exception(ctx, e)
|
// 异常冒泡到全局错误处理器
|
||||||
|
ctx.fail(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,121 @@
|
|||||||
|
package org.aikrai.vertx.http
|
||||||
|
|
||||||
|
import com.google.inject.Singleton
|
||||||
|
import io.vertx.core.Handler
|
||||||
|
import io.vertx.core.http.HttpHeaders
|
||||||
|
import io.vertx.ext.web.RoutingContext
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.aikrai.vertx.constant.HttpStatus
|
||||||
|
import org.aikrai.vertx.jackson.JsonUtil
|
||||||
|
import org.aikrai.vertx.utlis.Meta
|
||||||
|
import org.slf4j.MDC
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局错误处理器,负责捕获并处理所有未捕获的异常
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class GlobalErrorHandler : Handler<RoutingContext> {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
override fun handle(ctx: RoutingContext) {
|
||||||
|
val failure = ctx.failure()
|
||||||
|
val statusCode = determineStatusCode(ctx, failure)
|
||||||
|
val requestId = ctx.get<String>("requestId") ?: "N/A"
|
||||||
|
|
||||||
|
// 记录错误日志
|
||||||
|
logError(ctx, failure, statusCode, requestId)
|
||||||
|
|
||||||
|
// 构建标准错误响应
|
||||||
|
val apiResponse = buildErrorResponse(failure, statusCode)
|
||||||
|
apiResponse.requestId = requestId
|
||||||
|
|
||||||
|
// 发送响应
|
||||||
|
if (!ctx.response().ended()) {
|
||||||
|
val responseJson = try {
|
||||||
|
JsonUtil.toJsonStr(apiResponse)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error(e) { "序列化错误响应失败 (请求ID: $requestId)" }
|
||||||
|
// 回退到简单JSON
|
||||||
|
"""{"code":500,"message":"内部服务器错误 - 无法序列化错误响应","data":null,"requestId":"$requestId","timestamp":${System.currentTimeMillis()}}"""
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.put("responseData", responseJson) // 存储响应内容用于日志
|
||||||
|
|
||||||
|
ctx.response()
|
||||||
|
.setStatusCode(statusCode)
|
||||||
|
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
|
||||||
|
.end(responseJson)
|
||||||
|
} else {
|
||||||
|
logger.warn { "请求 ${ctx.request().uri()} 的响应已结束 (请求ID: $requestId)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确定HTTP状态码
|
||||||
|
*/
|
||||||
|
private fun determineStatusCode(ctx: RoutingContext, failure: Throwable?): Int {
|
||||||
|
// 优先使用RoutingContext中设置的状态码
|
||||||
|
if (ctx.statusCode() >= 400) {
|
||||||
|
return ctx.statusCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据异常类型确定状态码
|
||||||
|
return when (failure) {
|
||||||
|
is Meta -> HttpStatusMapping.getCode(failure.name, HttpStatus.ERROR)
|
||||||
|
is IllegalArgumentException -> HttpStatus.BAD_REQUEST
|
||||||
|
// 可添加更多异常类型的映射
|
||||||
|
else -> HttpStatus.ERROR // 默认为500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建标准错误响应
|
||||||
|
*/
|
||||||
|
private fun buildErrorResponse(failure: Throwable?, statusCode: Int): RespBean<Any?> {
|
||||||
|
return when (failure) {
|
||||||
|
null -> RespBean.error(statusCode, "发生未知错误")
|
||||||
|
is Meta -> RespBean.fromException(failure, statusCode)
|
||||||
|
else -> RespBean.fromException(failure, statusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录错误日志
|
||||||
|
*/
|
||||||
|
private fun logError(ctx: RoutingContext, failure: Throwable?, statusCode: Int, requestId: String) {
|
||||||
|
val request = ctx.request()
|
||||||
|
val uri = request.uri()
|
||||||
|
val method = request.method().name()
|
||||||
|
val remoteAddr = request.remoteAddress()?.host()
|
||||||
|
|
||||||
|
// 将请求ID放入MDC
|
||||||
|
MDC.put("requestId", requestId)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val logMessage = buildString {
|
||||||
|
append("处理请求失败 - ")
|
||||||
|
append("请求ID: $requestId, ")
|
||||||
|
append("方法: $method, ")
|
||||||
|
append("URI: $uri, ")
|
||||||
|
append("客户端IP: $remoteAddr, ")
|
||||||
|
append("状态码: $statusCode")
|
||||||
|
if (failure != null) {
|
||||||
|
append(", 异常类型: ${failure::class.java.name}")
|
||||||
|
if (!failure.message.isNullOrBlank()) {
|
||||||
|
append(", 消息: ${failure.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据状态码选择日志级别
|
||||||
|
if (statusCode >= 500 && failure != null) {
|
||||||
|
logger.error(failure) { logMessage } // 记录带堆栈的日志
|
||||||
|
} else {
|
||||||
|
logger.warn { logMessage } // 400级别错误只记录警告
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// 清理MDC
|
||||||
|
MDC.remove("requestId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,166 @@
|
|||||||
|
package org.aikrai.vertx.http
|
||||||
|
|
||||||
|
import cn.hutool.core.lang.Snowflake
|
||||||
|
import com.google.inject.Inject
|
||||||
|
import io.vertx.core.Handler
|
||||||
|
import io.vertx.core.http.HttpHeaders
|
||||||
|
import io.vertx.core.http.HttpMethod
|
||||||
|
import io.vertx.core.json.JsonObject
|
||||||
|
import io.vertx.ext.web.RoutingContext
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.aikrai.vertx.jackson.JsonUtil
|
||||||
|
import org.aikrai.vertx.utlis.IpUtil
|
||||||
|
import org.slf4j.MDC
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求日志处理器,负责生成请求ID,记录请求和响应的详细信息
|
||||||
|
*/
|
||||||
|
class RequestLogHandler @Inject constructor(
|
||||||
|
private val snowflake: Snowflake
|
||||||
|
) : Handler<RoutingContext> {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
override fun handle(ctx: RoutingContext) {
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
// 生成请求ID
|
||||||
|
val requestId = snowflake.nextIdStr()
|
||||||
|
ctx.put("requestId", requestId)
|
||||||
|
|
||||||
|
// 将请求ID放入MDC
|
||||||
|
MDC.put("requestId", requestId)
|
||||||
|
|
||||||
|
// 记录基本请求信息
|
||||||
|
val request = ctx.request()
|
||||||
|
val method = request.method()
|
||||||
|
val path = request.path()
|
||||||
|
val remoteAddr = IpUtil.getIpAddr(request) // 使用工具类获取真实IP
|
||||||
|
|
||||||
|
// 将基本信息放入MDC
|
||||||
|
MDC.put("method", method.name())
|
||||||
|
MDC.put("path", path)
|
||||||
|
MDC.put("remoteAddr", remoteAddr)
|
||||||
|
|
||||||
|
// 记录开始日志
|
||||||
|
logger.info { "请求开始 - 方法: $method, 路径: $path, 客户端IP: $remoteAddr, 请求ID: $requestId" }
|
||||||
|
|
||||||
|
// 在请求结束时记录详细日志
|
||||||
|
ctx.response().endHandler {
|
||||||
|
val endTime = System.currentTimeMillis()
|
||||||
|
val duration = endTime - startTime
|
||||||
|
val response = ctx.response()
|
||||||
|
val statusCode = response.statusCode
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构建详细日志数据
|
||||||
|
val logData = JsonObject()
|
||||||
|
.put("timestamp", Instant.ofEpochMilli(endTime).toString())
|
||||||
|
.put("requestId", requestId)
|
||||||
|
.put("method", method.name())
|
||||||
|
.put("uri", request.uri())
|
||||||
|
.put("path", path)
|
||||||
|
.put("statusCode", statusCode)
|
||||||
|
.put("durationMs", duration)
|
||||||
|
.put("remoteAddr", remoteAddr)
|
||||||
|
.put("userAgent", request.getHeader(HttpHeaders.USER_AGENT))
|
||||||
|
|
||||||
|
// 尝试获取用户信息
|
||||||
|
val userId = ctx.user()?.principal()?.getString("sub")
|
||||||
|
if (userId != null) {
|
||||||
|
logData.put("userId", userId)
|
||||||
|
MDC.put("userId", userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 视请求方法,可能记录查询参数
|
||||||
|
if (method == HttpMethod.GET || method == HttpMethod.DELETE) {
|
||||||
|
val queryParams = request.params().iterator().asSequence()
|
||||||
|
.map { it.key to it.value }
|
||||||
|
.toMap()
|
||||||
|
if (queryParams.isNotEmpty()) {
|
||||||
|
logData.put("queryParams", JsonObject(queryParams))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据内容类型,可能记录请求体(小心处理敏感信息)
|
||||||
|
if (method == HttpMethod.POST || method == HttpMethod.PUT || method == HttpMethod.PATCH) {
|
||||||
|
val contentType = request.getHeader(HttpHeaders.CONTENT_TYPE)
|
||||||
|
if (contentType?.contains("application/json") == true) {
|
||||||
|
val body = ctx.body().asString()
|
||||||
|
if (!body.isNullOrBlank()) {
|
||||||
|
try {
|
||||||
|
val bodyJson = JsonObject(body)
|
||||||
|
// 处理敏感字段,如密码
|
||||||
|
val sanitizedBody = sanitizeSensitiveData(bodyJson)
|
||||||
|
logData.put("requestBody", sanitizedBody)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logData.put("requestBodyRaw", "无法解析为JSON: " + body.take(100) + "...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试获取和记录响应数据
|
||||||
|
val responseData = ctx.get<String>("responseData")
|
||||||
|
if (!responseData.isNullOrBlank()) {
|
||||||
|
try {
|
||||||
|
val responseJson = JsonObject(responseData)
|
||||||
|
logData.put("responseBody", responseJson)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logData.put("responseBodyRaw", responseData.take(100) + "...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据状态码选择日志级别
|
||||||
|
MDC.put("statusCode", statusCode.toString())
|
||||||
|
MDC.put("duration", duration.toString())
|
||||||
|
|
||||||
|
val logMessage = buildString {
|
||||||
|
append("请求完成 - ")
|
||||||
|
append("方法: $method, ")
|
||||||
|
append("路径: $path, ")
|
||||||
|
append("状态码: $statusCode, ")
|
||||||
|
append("耗时: ${duration}ms, ")
|
||||||
|
append("请求ID: $requestId")
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
statusCode >= 500 -> logger.error { logMessage }
|
||||||
|
statusCode >= 400 -> logger.warn { logMessage }
|
||||||
|
else -> logger.info { logMessage }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 以JSON格式记录详细信息(可根据需要启用)
|
||||||
|
logger.debug { "请求详细信息: ${JsonUtil.toJsonStr(logData)}" }
|
||||||
|
} finally {
|
||||||
|
// 清理MDC
|
||||||
|
MDC.remove("requestId")
|
||||||
|
MDC.remove("method")
|
||||||
|
MDC.remove("path")
|
||||||
|
MDC.remove("remoteAddr")
|
||||||
|
MDC.remove("userId")
|
||||||
|
MDC.remove("statusCode")
|
||||||
|
MDC.remove("duration")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 继续下一个处理器
|
||||||
|
ctx.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理敏感数据,避免在日志中记录敏感信息
|
||||||
|
*/
|
||||||
|
private fun sanitizeSensitiveData(json: JsonObject): JsonObject {
|
||||||
|
val result = json.copy()
|
||||||
|
val sensitiveFields = listOf("password", "passwordConfirm", "oldPassword", "newPassword", "token", "accessToken", "refreshToken")
|
||||||
|
|
||||||
|
for (field in sensitiveFields) {
|
||||||
|
if (result.containsKey(field)) {
|
||||||
|
result.put(field, "******")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
150
vertx-fw/src/main/kotlin/org/aikrai/vertx/http/RespBean.kt
Normal file
150
vertx-fw/src/main/kotlin/org/aikrai/vertx/http/RespBean.kt
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
package org.aikrai.vertx.http
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude
|
||||||
|
import org.aikrai.vertx.constant.HttpStatus
|
||||||
|
import org.aikrai.vertx.utlis.Meta
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准API响应格式,用于所有HTTP响应
|
||||||
|
*
|
||||||
|
* @param code 状态码
|
||||||
|
* @param message 消息
|
||||||
|
* @param data 数据(可为null)
|
||||||
|
* @param requestId 请求ID(用于跟踪请求)
|
||||||
|
*/
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
data class RespBean<T>(
|
||||||
|
val code: Int,
|
||||||
|
val message: String,
|
||||||
|
val data: T? = null,
|
||||||
|
var requestId: String? = null,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* 创建成功响应
|
||||||
|
*
|
||||||
|
* @param data 响应数据
|
||||||
|
* @param message 成功消息
|
||||||
|
* @param code 状态码,默认为HttpStatus.SUCCESS
|
||||||
|
* @return ApiResponse实例
|
||||||
|
*/
|
||||||
|
fun <T> success(data: T? = null, message: String = "Success", code: Int = HttpStatus.SUCCESS): RespBean<T> {
|
||||||
|
val finalCode = if (data == null && code == HttpStatus.SUCCESS) HttpStatus.NO_CONTENT else code
|
||||||
|
return RespBean(
|
||||||
|
code = finalCode,
|
||||||
|
message = message,
|
||||||
|
data = data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建错误响应
|
||||||
|
*
|
||||||
|
* @param code 错误码
|
||||||
|
* @param message 错误消息
|
||||||
|
* @param data 错误相关数据(可选)
|
||||||
|
* @return ApiResponse实例
|
||||||
|
*/
|
||||||
|
fun <T> error(code: Int = HttpStatus.ERROR, message: String, data: T? = null): RespBean<T> {
|
||||||
|
return RespBean(
|
||||||
|
code = code,
|
||||||
|
message = message,
|
||||||
|
data = data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从异常创建错误响应
|
||||||
|
*
|
||||||
|
* @param exception 异常
|
||||||
|
* @param defaultStatusCode 默认状态码,如果无法从异常确定状态码
|
||||||
|
* @return ApiResponse实例
|
||||||
|
*/
|
||||||
|
fun <T> fromException(
|
||||||
|
exception: Throwable,
|
||||||
|
defaultStatusCode: Int = HttpStatus.ERROR
|
||||||
|
): RespBean<T> {
|
||||||
|
// 确定状态码和消息
|
||||||
|
var statusCode = defaultStatusCode
|
||||||
|
val errorName: String
|
||||||
|
val errorMessage: String
|
||||||
|
val errorData: Any?
|
||||||
|
|
||||||
|
when (exception) {
|
||||||
|
is Meta -> {
|
||||||
|
// 根据Meta.name确定状态码
|
||||||
|
statusCode = when (exception.name) {
|
||||||
|
"Unauthorized" -> HttpStatus.UNAUTHORIZED
|
||||||
|
"Forbidden" -> HttpStatus.FORBIDDEN
|
||||||
|
"NotFound" -> HttpStatus.NOT_FOUND
|
||||||
|
"RequiredArgument", "InvalidArgument", "BadRequest" -> HttpStatus.BAD_REQUEST
|
||||||
|
"Timeout" -> HttpStatus.ERROR // 使用ERROR作为超时状态码
|
||||||
|
"NotSupported" -> HttpStatus.UNSUPPORTED_TYPE
|
||||||
|
"Unimplemented" -> HttpStatus.NOT_IMPLEMENTED
|
||||||
|
else -> defaultStatusCode
|
||||||
|
}
|
||||||
|
errorName = exception.name
|
||||||
|
errorMessage = exception.message
|
||||||
|
errorData = exception.data
|
||||||
|
}
|
||||||
|
is IllegalArgumentException -> {
|
||||||
|
statusCode = HttpStatus.BAD_REQUEST
|
||||||
|
errorName = "BadRequest"
|
||||||
|
errorMessage = exception.message ?: "Invalid argument"
|
||||||
|
errorData = null
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// 通用异常处理
|
||||||
|
errorName = exception.javaClass.simpleName
|
||||||
|
errorMessage = exception.message ?: "Internal Server Error"
|
||||||
|
errorData = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组合错误名称和消息
|
||||||
|
val finalMessage = if (errorMessage.contains(errorName, ignoreCase = true)) {
|
||||||
|
errorMessage
|
||||||
|
} else {
|
||||||
|
"$errorName: $errorMessage"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return RespBean(
|
||||||
|
code = statusCode,
|
||||||
|
message = finalMessage,
|
||||||
|
data = errorData as? T
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP状态码映射,用于将Meta.name映射到HTTP状态码
|
||||||
|
*/
|
||||||
|
object HttpStatusMapping {
|
||||||
|
private val mapping = mapOf(
|
||||||
|
"Unauthorized" to HttpStatus.UNAUTHORIZED,
|
||||||
|
"Forbidden" to HttpStatus.FORBIDDEN,
|
||||||
|
"NotFound" to HttpStatus.NOT_FOUND,
|
||||||
|
"RequiredArgument" to HttpStatus.BAD_REQUEST,
|
||||||
|
"InvalidArgument" to HttpStatus.BAD_REQUEST,
|
||||||
|
"BadRequest" to HttpStatus.BAD_REQUEST,
|
||||||
|
"Timeout" to HttpStatus.ERROR, // 使用ERROR作为超时状态码
|
||||||
|
"Repository" to HttpStatus.ERROR,
|
||||||
|
"Unimplemented" to HttpStatus.NOT_IMPLEMENTED,
|
||||||
|
"NotSupported" to HttpStatus.UNSUPPORTED_TYPE
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取与Meta.name对应的HTTP状态码
|
||||||
|
*
|
||||||
|
* @param name Meta.name或其前缀
|
||||||
|
* @param defaultCode 默认状态码
|
||||||
|
* @return HTTP状态码
|
||||||
|
*/
|
||||||
|
fun getCode(name: String, defaultCode: Int = HttpStatus.ERROR): Int {
|
||||||
|
// 检查是否包含前缀,如"Repository:"
|
||||||
|
val baseName = name.substringBefore(':')
|
||||||
|
return mapping[baseName] ?: mapping[name] ?: defaultCode
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
package org.aikrai.vertx.resp
|
||||||
|
|
||||||
|
import io.vertx.core.http.HttpHeaders
|
||||||
|
import io.vertx.ext.web.RoutingContext
|
||||||
|
import org.aikrai.vertx.http.RespBean
|
||||||
|
import org.aikrai.vertx.jackson.JsonUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认响应处理器实现
|
||||||
|
*/
|
||||||
|
class DefaultResponseHandler : ResponseHandlerInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理成功响应
|
||||||
|
*/
|
||||||
|
override suspend fun handle(
|
||||||
|
ctx: RoutingContext,
|
||||||
|
responseData: Any?,
|
||||||
|
customizeResponse: Boolean
|
||||||
|
) {
|
||||||
|
val requestId = ctx.get<String>("requestId")
|
||||||
|
|
||||||
|
// 使用RespBean包装响应数据
|
||||||
|
val respBean = RespBean.success(responseData)
|
||||||
|
respBean.requestId = requestId
|
||||||
|
|
||||||
|
val resStr = JsonUtil.toJsonStr(respBean)
|
||||||
|
// 存储响应内容用于日志
|
||||||
|
ctx.put("responseData", resStr)
|
||||||
|
|
||||||
|
// 如果需要自定义响应,则不发送标准响应
|
||||||
|
if (customizeResponse) return
|
||||||
|
|
||||||
|
// 发送标准成功响应
|
||||||
|
ctx.response()
|
||||||
|
.setStatusCode(respBean.code)
|
||||||
|
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
|
||||||
|
.end(resStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package org.aikrai.vertx.resp
|
||||||
|
|
||||||
|
import io.vertx.ext.web.RoutingContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应处理器接口,负责处理API响应
|
||||||
|
*/
|
||||||
|
interface ResponseHandlerInterface {
|
||||||
|
/**
|
||||||
|
* 处理成功响应
|
||||||
|
*
|
||||||
|
* @param ctx 路由上下文
|
||||||
|
* @param responseData 响应数据
|
||||||
|
* @param customizeResponse 是否自定义响应,如果为true则由控制器自行处理响应
|
||||||
|
*/
|
||||||
|
suspend fun handle(ctx: RoutingContext, responseData: Any?, customizeResponse: Boolean = false)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user