refactor(router): 优化路由构建器

This commit is contained in:
AiKrai 2025-03-13 14:38:33 +08:00
parent fa18c92a02
commit 5eccf7ceed
3 changed files with 251 additions and 219 deletions

View File

@ -1,5 +1,6 @@
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
@ -8,7 +9,6 @@ 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.openapi.OpenApiSpecGenerator
class ApifoxClient @Inject constructor(
private val vertx: Vertx,

View File

@ -1,4 +1,4 @@
package org.aikrai.vertx.openapi
package app.util.openapi
import cn.hutool.core.util.StrUtil
import io.swagger.v3.core.util.Json
@ -14,7 +14,6 @@ import io.swagger.v3.oas.models.parameters.Parameter
import io.swagger.v3.oas.models.parameters.RequestBody
import io.swagger.v3.oas.models.responses.ApiResponse
import io.swagger.v3.oas.models.responses.ApiResponses
import io.swagger.v3.oas.models.security.SecurityScheme
import io.swagger.v3.oas.models.servers.Server
import mu.KotlinLogging
import org.aikrai.vertx.context.Controller
@ -25,7 +24,6 @@ import org.aikrai.vertx.utlis.ClassUtil
import org.reflections.Reflections
import java.lang.reflect.Method
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import java.sql.Date
import java.sql.Time
import java.sql.Timestamp
@ -219,7 +217,9 @@ class OpenApiSpecGenerator {
private fun buildPath(controllerPrefix: String, methodName: String): String {
val classPath = if (controllerPrefix != "/") {
StrUtil.toCamelCase(StrUtil.toUnderlineCase(controllerPrefix))
} else ""
} else {
""
}
val methodPath = StrUtil.toCamelCase(StrUtil.toUnderlineCase(methodName))
return "/$classPath/$methodPath".replace("//", "/")
}

View File

@ -1,7 +1,6 @@
package org.aikrai.vertx.context
import cn.hutool.core.util.StrUtil
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.vertx.core.http.HttpMethod
import io.vertx.core.json.JsonObject
import io.vertx.ext.auth.User
@ -10,6 +9,7 @@ import io.vertx.ext.web.RoutingContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.aikrai.vertx.auth.*
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
@ -19,209 +19,247 @@ import org.aikrai.vertx.utlis.Meta
import org.reflections.Reflections
import java.lang.reflect.Method
import java.lang.reflect.ParameterizedType
import kotlin.collections.ArrayList
import kotlin.coroutines.Continuation
import kotlin.reflect.KFunction
import kotlin.reflect.full.callSuspend
import kotlin.reflect.full.declaredFunctions
import kotlin.reflect.jvm.javaType
/**
* RouterBuilder - 基于注解控制器构建Vert.x路由的实用工具
*
* 该类扫描控制器类分析其方法并将其注册为具有适当参数绑定和授权规则的HTTP端点
*/
class RouterBuilder(
private val coroutineScope: CoroutineScope,
private val router: Router,
private val scanPath: String? = null,
private val responseHandler: ResponseHandlerInterface = DefaultResponseHandler()
) {
var anonymousPaths = ArrayList<String>()
// 不需要认证的路径集合
val anonymousPaths = mutableListOf<String>()
/**
* 基于注解控制器类构建路由
*
* @param getIt 解析控制器实例的函数
* @return 当前RouterBuilder实例用于链式调用
*/
fun build(getIt: (clazz: Class<*>) -> Any): RouterBuilder {
// 缓存路由信息
// 扫描并缓存控制器路由信息
val routeInfoCache = scanControllerRoutes()
// 注册路由处理器
registerRouteHandlers(routeInfoCache, getIt)
return this
}
/**
* 扫描控制器类并提取路由信息
*/
private fun scanControllerRoutes(): Map<Pair<String, HttpMethod>, RouteInfo> {
val routeInfoCache = mutableMapOf<Pair<String, HttpMethod>, RouteInfo>()
// 获取所有 Controller 类中的公共方法
val packagePath = scanPath ?: ClassUtil.getMainClass().packageName
val controllerClassSet = Reflections(packagePath).getTypesAnnotatedWith(Controller::class.java)
val controllerMethods = ClassUtil.getPublicMethods(controllerClassSet)
for ((classType, methods) in controllerMethods) {
processControllerClass(classType, methods.toList(), routeInfoCache)
}
return routeInfoCache
}
/**
* 处理控制器类及其方法
*/
private fun processControllerClass(
classType: Class<*>,
methods: List<Method>,
routeInfoCache: MutableMap<Pair<String, HttpMethod>, RouteInfo>
) {
val controllerAnnotation = classType.getDeclaredAnnotationsByType(Controller::class.java).firstOrNull()
val prefixPath = controllerAnnotation?.prefix ?: ""
val classAllowAnonymous = classType.getAnnotation(AllowAnonymous::class.java) != null
for (method in methods) {
val reqPath = getReqPath(prefixPath, classType, method)
val httpMethod = getHttpMethod(method)
val allowAnonymous = method.getAnnotation(AllowAnonymous::class.java) != null
if (classAllowAnonymous || allowAnonymous) anonymousPaths.add(reqPath)
// 处理匿名访问
if (classAllowAnonymous || method.getAnnotation(AllowAnonymous::class.java) != null) {
anonymousPaths.add(reqPath)
}
// 提取方法元数据
val customizeResp = method.getAnnotation(CustomizeResponse::class.java) != null
val role = method.getAnnotation(CheckRole::class.java)
val permissions = method.getAnnotation(CheckPermission::class.java)
val kFunction = classType.kotlin.declaredFunctions.find { it.name == method.name }
if (kFunction != null) {
val parameterInfo = kFunction.parameters.mapNotNull { parameter ->
// 查找对应的Kotlin函数
classType.kotlin.declaredFunctions.find { it.name == method.name }?.let { kFunction ->
val parameterInfo = extractParameterInfo(kFunction)
routeInfoCache[reqPath to httpMethod] = RouteInfo(
classType, method, kFunction, parameterInfo,
customizeResp, role, permissions, httpMethod
)
}
}
}
/**
* 从Kotlin函数中提取参数信息
*/
private fun extractParameterInfo(kFunction: KFunction<*>): List<ParameterInfo> {
return kFunction.parameters.mapNotNull { parameter ->
val javaType = parameter.type.javaType
// 跳过协程的Continuation参数
if (javaType is Class<*> && Continuation::class.java.isAssignableFrom(javaType)) {
return@mapNotNull null
}
parameter.name ?: return@mapNotNull null
// 参数必须具有名称
val paramName = parameter.name ?: return@mapNotNull null
// 从D注解获取自定义参数名
val annotation = parameter.annotations.find { it is D } as? D
val paramName = annotation?.name?.takeIf { it.isNotBlank() } ?: parameter.name ?: ""
val finalParamName = annotation?.name?.takeIf { it.isNotBlank() } ?: paramName
// 确定参数类型
val typeClass = when (javaType) {
is Class<*> -> javaType
is ParameterizedType -> javaType.rawType as? Class<*>
else -> null
}
} ?: parameter.type.javaType as Class<*>
// 处理枚举类型参数
val isEnum = typeClass.isEnum || parameter.type.javaType.javaClass.isEnum
val enumValueMethod = if (isEnum) {
typeClass.methods.find { it.isAnnotationPresent(EnumValue::class.java) }
} else null
val enumConstants = if (isEnum) {
typeClass.enumConstants?.associateBy({
enumValueMethod?.invoke(it)?.toString() ?: (it as Enum<*>).name
}, { it })
} else emptyMap()
// 检查是否是复杂类型
val isComplex = !parameter.type.classifier.toString().startsWith("class kotlin.") &&
!parameter.type.classifier.toString().startsWith("class io.vertx") &&
!typeClass.isEnum &&
!parameter.type.javaType.javaClass.isEnum &&
parameter.type.javaType is Class<*>
ParameterInfo(
name = paramName,
type = typeClass ?: parameter.type.javaType as Class<*>,
name = finalParamName,
type = typeClass,
isNullable = parameter.type.isMarkedNullable,
isList = parameter.type.classifier == List::class,
isComplex = !parameter.type.classifier.toString().startsWith("class kotlin.") &&
!parameter.type.classifier.toString().startsWith("class io.vertx") &&
!(parameter.type.javaType as Class<*>).isEnum &&
!parameter.type.javaType.javaClass.isEnum &&
parameter.type.javaType is Class<*>,
isEnum = typeClass?.isEnum ?: (parameter.type.javaType as Class<*>).isEnum
isComplex = isComplex,
isEnum = isEnum,
enumValueMethod = enumValueMethod,
enumConstants = enumConstants
)
}
routeInfoCache[reqPath to httpMethod] =
RouteInfo(classType, method, kFunction, parameterInfo, customizeResp, role, permissions)
}
/**
* 为缓存的路由注册路由处理程序
*/
private fun registerRouteHandlers(
routeInfoCache: Map<Pair<String, HttpMethod>, RouteInfo>,
getIt: (clazz: Class<*>) -> Any
) {
routeInfoCache.forEach { (pathMethod, routeInfo) ->
val (path, _) = pathMethod
router.route(routeInfo.httpMethod, path).handler { ctx ->
handleRequest(ctx, getIt, routeInfo)
}
}
}
// 注册路由处理器
routeInfoCache.forEach { (path, routeInfo) ->
router.route(routeInfo.httpMethod, path.first).handler { ctx ->
if (ctx.user() != null) {
val user = ctx.user() as AuthUser
if (!user.validateAuth(routeInfo)) {
ctx.fail(403, Meta.unauthorized("unauthorized"))
return@handler
/**
* 处理传入的HTTP请求
*/
private fun handleRequest(ctx: RoutingContext, getIt: (clazz: Class<*>) -> Any, routeInfo: RouteInfo) {
// 如果存在用户,检查授权
ctx.user()?.let { user ->
if (user is AuthUser) {
try {
user.validateAuth(routeInfo.role, routeInfo.permissions)
} catch (e: Throwable) {
ctx.fail(403, Meta.unauthorized("未授权"))
return
}
}
}
// 获取控制器实例并执行方法
val instance = getIt(routeInfo.classType)
buildLambda(ctx, instance, routeInfo)
}
}
return this
executeControllerMethod(ctx, instance, routeInfo)
}
private fun buildLambda(ctx: RoutingContext, instance: Any, routeInfo: RouteInfo) {
/**
* 在协程中执行控制器方法
*/
private fun executeControllerMethod(ctx: RoutingContext, instance: Any, routeInfo: RouteInfo) {
coroutineScope.launch {
try {
val params = getParamsInstance(ctx, routeInfo.parameterInfo)
val resObj = if (routeInfo.kFunction.isSuspend) {
val params = resolveMethodParameters(ctx, routeInfo.parameterInfo)
val result = if (routeInfo.kFunction.isSuspend) {
routeInfo.kFunction.callSuspend(instance, *params)
} else {
routeInfo.kFunction.call(instance, *params)
}
responseHandler.normal(ctx, resObj, routeInfo.customizeResp)
responseHandler.normal(ctx, result, routeInfo.customizeResp)
} catch (e: Throwable) {
responseHandler.exception(ctx, e)
}
}
}
companion object {
private val objectMapper = jacksonObjectMapper()
private fun AuthUser.validateAuth(routeInfo: RouteInfo): Boolean {
// 如果没有权限要求直接返回true
if (routeInfo.role == null && routeInfo.permissions == null) return true
// 验证角色
val hasValidRole = routeInfo.role?.let { role ->
val roleSet = attributes().getJsonArray("role").toSet() as Set<String>
if (roleSet.isEmpty()) {
false
} else {
val reqRoleSet = (role.value + role.type).filter { it.isNotBlank() }.toSet()
validateSet(reqRoleSet, roleSet, role.mode)
}
} ?: true
// 验证权限
val hasValidPermission = routeInfo.permissions?.let { permissions ->
val permissionSet = attributes().getJsonArray("permissions").toSet() as Set<String>
val roleSet = attributes().getJsonArray("role").toSet() as Set<String>
if (permissionSet.isEmpty() && roleSet.isEmpty()) {
false
} else {
if (permissions.orRole.isNotEmpty()) {
val roleBoolean = validateSet(permissions.orRole.toSet(), roleSet, Mode.AND)
if (roleBoolean) return true
}
val reqPermissionSet = (permissions.value + permissions.type).filter { it.isNotBlank() }.toSet()
validateSet(reqPermissionSet, permissionSet, permissions.mode)
}
} ?: true
return hasValidRole && hasValidPermission
}
private fun validateSet(
required: Set<String>,
actual: Set<String>,
mode: Mode
): Boolean {
if (required.isEmpty()) return true
return when (mode) {
Mode.AND -> required == actual
Mode.OR -> required.any { it in actual }
}
}
private fun getReqPath(prefix: String, clazz: Class<*>, method: Method): String {
var classPath = if (prefix.isNotBlank()) {
StrUtil.toCamelCase(StrUtil.toUnderlineCase(prefix))
} else {
StrUtil.toCamelCase(StrUtil.toUnderlineCase(clazz.simpleName.removeSuffix("Controller")))
}
if (classPath == "/") classPath = ""
val methodName = StrUtil.toCamelCase(StrUtil.toUnderlineCase(method.name))
return "/$classPath/$methodName".replace("//", "/")
}
private fun getParamsInstance(ctx: RoutingContext, paramsInfo: List<ParameterInfo>): Array<Any?> {
/**
* 从请求中解析方法参数
*/
private fun resolveMethodParameters(ctx: RoutingContext, paramsInfo: List<ParameterInfo>): Array<Any?> {
val params = mutableListOf<Any?>()
// 从不同来源收集参数
val formAttributes = ctx.request().formAttributes().associate { it.key to it.value }
val queryParams = ctx.queryParams().entries().associate { it.key to it.value }
val combinedParams = formAttributes + queryParams
// 解析Body
val bodyObj = if (!ctx.body().isEmpty) ctx.body().asJsonObject() else null
// 解析请求体
val bodyObj = ctx.body().takeUnless { it.isEmpty }?.asJsonObject()
val bodyMap = bodyObj?.map ?: emptyMap()
// 处理每个参数
paramsInfo.forEach { param ->
if (param.isList) {
when {
// 处理List类型参数
param.isList -> {
var value = ctx.queryParams().getAll(param.name)
if (value.isEmpty() && bodyMap[param.name] != null) {
value = (bodyMap[param.name] as Collection<*>).map { it.toString() }.toMutableList()
value = (bodyMap[param.name] as? Collection<*>)?.map { it.toString() }?.toMutableList() ?: mutableListOf()
}
if (value.isEmpty() && !param.isNullable) {
throw IllegalArgumentException("Missing required parameter: ${param.name}")
throw IllegalArgumentException("缺少必要参数: ${param.name}")
}
params.add(value.ifEmpty { null })
return@forEach
}
if (param.isEnum) {
val value = sequenceOf(
combinedParams[param.name],
bodyMap[param.name]
).filterNotNull().map { it.toString() }.firstOrNull()
val enumValueMethod = param.type.methods.find { method ->
method.isAnnotationPresent(EnumValue::class.java)
}
val enumValue = param.type.enumConstants.firstOrNull { enumConstant ->
if (enumValueMethod != null) {
enumValueMethod.invoke(enumConstant).toString() == value
} else {
(enumConstant as Enum<*>).name == value
}
}
if (enumValue != null) params.add(enumValue)
return@forEach
// 处理枚举类型参数
param.isEnum -> {
val value = combinedParams[param.name] ?: bodyMap[param.name]?.toString()
val enumValue = param.enumConstants?.get(value)
params.add(enumValue)
}
if (param.isComplex) {
// 处理复杂对象参数
param.isComplex -> {
try {
val value = sequenceOf(
if (paramsInfo.size == 1) bodyObj else null,
@ -229,68 +267,79 @@ class RouterBuilder(
combinedParams[param.name]?.let { JsonObject(it) },
bodyObj
).filterNotNull().firstOrNull { !it.isEmpty }
if (value?.isEmpty == true && !param.isNullable) {
throw IllegalArgumentException("Missing required parameter: ${param.name}")
throw IllegalArgumentException("缺少必要参数: ${param.name}")
}
params.add(if (value == null || value.isEmpty) null else JsonUtil.parseObject(value, param.type))
return@forEach
} catch (e: Exception) {
throw IllegalArgumentException(e.message, e)
}
}
params.add(
when (param.type) {
// 处理特殊或基本类型参数
else -> {
params.add(when (param.type) {
RoutingContext::class.java -> ctx
User::class.java -> ctx.user()
else -> {
val bodyValue = bodyMap[param.name]
val paramValue = bodyValue?.toString() ?: combinedParams[param.name]
when {
paramValue == null -> {
if (!param.isNullable) throw IllegalArgumentException("Missing required parameter: ${param.name}") else null
if (!param.isNullable) {
throw IllegalArgumentException("缺少必要参数: ${param.name}")
} else null
}
else -> {
val value = getParamValue(paramValue.toString(), param.type)
val value = convertStringToType(paramValue.toString(), param.type)
if (!param.isNullable && value == null) {
throw IllegalArgumentException("Missing required parameter: ${param.name}")
} else {
value
throw IllegalArgumentException("缺少必要参数: ${param.name}")
} else value
}
}
}
})
}
}
)
}
return params.toTypedArray()
}
companion object {
/**
* 将字符串参数值映射到目标类型
*
* @param paramValue 参数的字符串值
* @param type 目标 [Class] 类型
* @return 转换为目标类型的参数值如果转换失败则返回 `null`
* 根据类和方法信息构造请求路径
*/
private fun getParamValue(paramValue: String, type: Class<*>): Any? {
private fun getReqPath(prefix: String, clazz: Class<*>, method: Method): String {
val classPath = if (prefix.isNotBlank()) {
StrUtil.toCamelCase(StrUtil.toUnderlineCase(prefix))
} else {
StrUtil.toCamelCase(StrUtil.toUnderlineCase(clazz.simpleName.removeSuffix("Controller")))
}.let { if (it == "/") "" else it }
val methodName = StrUtil.toCamelCase(StrUtil.toUnderlineCase(method.name))
return "/$classPath/$methodName".replace("//", "/")
}
/**
* 将字符串值转换为目标类型
*/
private fun convertStringToType(paramValue: String, type: Class<*>): Any? {
return when (type) {
String::class.java -> paramValue
Int::class.java, Integer::class.java -> paramValue.toIntOrNull()
Long::class.java, Long::class.java -> paramValue.toLongOrNull()
Double::class.java, Double::class.java -> paramValue.toDoubleOrNull()
Boolean::class.java, Boolean::class.java -> paramValue.toBoolean()
Long::class.java, java.lang.Long::class.java -> paramValue.toLongOrNull()
Double::class.java, java.lang.Double::class.java -> paramValue.toDoubleOrNull()
Boolean::class.java, java.lang.Boolean::class.java -> paramValue.toBoolean()
else -> paramValue
}
}
/**
* 根据 [CustomizeRequest] 注解确定给定 REST 方法的 HTTP 方法
*
* @param method 目标方法
* @return 对应的 [HttpMethod]
* 确定控制器方法的HTTP方法
*/
fun getHttpMethod(method: Method): HttpMethod {
val api = method.getAnnotation(CustomizeRequest::class.java)
@ -306,33 +355,11 @@ class RouterBuilder(
HttpMethod.POST
}
}
}
/**
* 将对象序列化为 JSON 表示
*
* @param obj 要序列化的对象
* @return JSON 字符串
* 路由元数据数据类
*/
private fun serializeToJson(obj: Any?): String {
return objectMapper.writeValueAsString(obj)
}
private fun getEnumValue(enumValue: Any?): Any? {
if (enumValue == null || !enumValue::class.java.isEnum) {
return null // 不是枚举或为空,直接返回 null
}
val enumClass = enumValue::class.java
val methods = enumClass.declaredMethods
for (method in methods) {
if (method.isAnnotationPresent(EnumValue::class.java)) {
method.isAccessible = true // 如果方法是私有的,设置为可访问
return method.invoke(enumValue) // 调用带有 @EnumValue 注解的方法
}
}
return null // 没有找到带有 @EnumValue 注解的方法
}
}
private data class RouteInfo(
val classType: Class<*>,
val method: Method,
@ -341,15 +368,20 @@ class RouterBuilder(
val customizeResp: Boolean,
val role: CheckRole? = null,
val permissions: CheckPermission? = null,
val httpMethod: HttpMethod = getHttpMethod(method)
val httpMethod: HttpMethod
)
/**
* 参数元数据数据类
*/
private data class ParameterInfo(
val name: String,
val type: Class<*>,
val isNullable: Boolean,
val isList: Boolean,
val isComplex: Boolean,
val isEnum: Boolean
val isEnum: Boolean,
val enumValueMethod: Method? = null,
val enumConstants: Map<String, Any>? = null
)
}