From 024dd8c7cf67a975f1dbb45300a2fc99880f5d6d Mon Sep 17 00:00:00 2001 From: tursom Date: Sat, 14 Dec 2019 17:01:33 +0800 Subject: [PATCH] =?UTF-8?q?AsyncRoutedHttpHandler=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=AF=B9=E5=8D=8F=E7=A8=8B=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/cn/tursom/log/Slf4jEnhance.kt | 2 +- settings.gradle | 1 + .../cn/tursom/web/router/RoutedHttpHandler.kt | 207 +++++++++++------- .../web/router/AsyncRoutedHttpHandler.kt | 182 +++++++++++++++ 4 files changed, 309 insertions(+), 83 deletions(-) create mode 100644 web/web-coroutine/src/main/kotlin/cn/tursom/web/router/AsyncRoutedHttpHandler.kt diff --git a/log/src/main/kotlin/cn/tursom/log/Slf4jEnhance.kt b/log/src/main/kotlin/cn/tursom/log/Slf4jEnhance.kt index ef3e999..bb40c15 100644 --- a/log/src/main/kotlin/cn/tursom/log/Slf4jEnhance.kt +++ b/log/src/main/kotlin/cn/tursom/log/Slf4jEnhance.kt @@ -3,4 +3,4 @@ package cn.tursom.log import org.slf4j.Logger import org.slf4j.LoggerFactory -inline fun LoggerFactory.getLogger(): Logger = LoggerFactory.getLogger(T::class.java) \ No newline at end of file +inline fun T.slf4jLogger(): Logger = LoggerFactory.getLogger(T::class.java) diff --git a/settings.gradle b/settings.gradle index 9f4bf0a..7287001 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,4 +5,5 @@ include 'AsyncSocket' include 'log' include 'json' include 'utils:yaml' +include 'web:web-coroutine' diff --git a/web/src/main/kotlin/cn/tursom/web/router/RoutedHttpHandler.kt b/web/src/main/kotlin/cn/tursom/web/router/RoutedHttpHandler.kt index 8220b1b..279af84 100644 --- a/web/src/main/kotlin/cn/tursom/web/router/RoutedHttpHandler.kt +++ b/web/src/main/kotlin/cn/tursom/web/router/RoutedHttpHandler.kt @@ -27,10 +27,10 @@ import java.lang.reflect.Method @Suppress("MemberVisibilityCanBePrivate", "unused") open class RoutedHttpHandler( target: Any? = null, - val routerMaker: () -> Router<(HttpContent) -> Unit> = { SimpleRouter() } + val routerMaker: () -> Router Unit>> = { SimpleRouter() } ) : HttpHandler { - protected val router: Router<(HttpContent) -> Unit> = routerMaker() - protected val routerMap: HashMap Unit>> = HashMap() + protected val router: Router Unit>> = routerMaker() + protected val routerMap: HashMap Unit>>> = HashMap() init { @Suppress("LeakingThis") @@ -40,7 +40,7 @@ open class RoutedHttpHandler( override fun handle(content: HttpContent) = if (content is MutableHttpContent) { handle(content, getHandler(content, content.method, content.uri)) } else { - handle(content, getHandler(content.method, content.uri).first) + handle(content, getHandler(content.method, content.uri).first?.second) } open fun handle(content: HttpContent, handler: ((HttpContent) -> Unit)?) { @@ -55,11 +55,11 @@ open class RoutedHttpHandler( content.finish(404) } - fun addRouter(handler: Any) { + open fun addRouter(handler: Any) { @Suppress("LeakingThis") val clazz = handler.javaClass clazz.methods.forEach { method -> - log?.debug("try mapping {}", method) + log?.debug("try mapping {}({})", method, method.parameterTypes) method.parameterTypes.let { if (!(it.size == 1 && HttpContent::class.java.isAssignableFrom(it[0])) && it.isNotEmpty()) { return@forEach @@ -69,103 +69,143 @@ open class RoutedHttpHandler( } } - fun addRouter(route: String, handler: (HttpContent) -> Unit) { - router[safeRoute(route)] = handler + fun addRouter(route: String, handler: (HttpContent) -> Unit) = addRouter(route, null, handler) + fun addRouter(route: String, obj: Any?, handler: (HttpContent) -> Unit) { + router[safeRoute(route)] = obj to handler } - fun addRouter(method: String, route: String, handler: (HttpContent) -> Unit) { - getRouter(method)[safeRoute(route)] = handler + fun addRouter(method: String, route: String, handler: (HttpContent) -> Unit) = addRouter(method, route, null, handler) + fun addRouter(method: String, route: String, obj: Any?, handler: (HttpContent) -> Unit) { + getRouter(method)[safeRoute(route)] = obj to handler } fun getHandler(content: MutableHttpContent, method: String, route: String): ((HttpContent) -> Unit)? { - val router = getHandler(method, route) + val safeRoute = safeRoute(route) + val router = getHandler(method, safeRoute) if (router.first != null) { router.second.forEach { (k, v) -> content.addParam(k, v) } } - return router.first ?: this.router[route].first + return router.first?.second ?: this.router[safeRoute].first?.second } - fun getHandler(method: String, route: String): Pair<((HttpContent) -> Unit)?, List>> { - val router = getRouter(method)[route] - return if (router.first != null) router else this.router[route] + fun getHandler(method: String, route: String): Pair Unit>?, List>> { + val safeRoute = safeRoute(route) + val router = getRouter(method)[safeRoute] + return if (router.first != null) router else this.router[safeRoute] + } + + protected fun insertMapping(obj: Any, method: Method) { + method.annotations.forEach { annotation -> + log?.info("method route {} annotation {}", method, annotation) + val (routes, router) = getRoutes(annotation) ?: return@forEach + log?.info("method route {} mapped to {}", method, routes) + routes.forEach { route -> + router[safeRoute(route)] = if (method.parameterTypes.isEmpty()) { + obj to when { + method.getAnnotation(Html::class.java) != null -> { content -> + method(obj)?.let { result -> finishHtml(result, content) } + } + method.getAnnotation(Text::class.java) != null -> { content -> + method(obj)?.let { result -> finishText(result, content) } + } + method.getAnnotation(Json::class.java) != null -> { content -> + method(obj)?.let { result -> finishJson(result, content) } + } + else -> { content -> + method(obj)?.let { result -> autoReturn(result, content) } + } + } + } else obj to when (method.returnType) { + Void::class.java -> { content -> method(obj, content) } + Void.TYPE -> { content -> method(obj, content) } + Unit::class.java -> { content -> method(obj, content) } + else -> when { + method.getAnnotation(Html::class.java) != null -> { content -> + method(obj, content)?.let { result -> finishHtml(result, content) } + } + method.getAnnotation(Text::class.java) != null -> { content -> + method(obj, content)?.let { result -> finishText(result, content) } + } + method.getAnnotation(Json::class.java) != null -> { content -> + method(obj, content)?.let { result -> finishJson(result, content) } + } + else -> { content -> + method(obj, content)?.let { result -> autoReturn(result, content) } + } + } + } + } + } } fun deleteRouter(route: String, method: String) { getRouter(method).delRoute(safeRoute(route)) } - protected fun insertMapping(obj: Any, method: Method) { - method.annotations.forEach { annotation -> - val routes: Array - val router: Router<(HttpContent) -> Unit> - when (annotation) { - is Mapping -> { - routes = annotation.route - router = getRouter(annotation.method) + fun deleteRouter(handler: Any) { + @Suppress("LeakingThis") + val clazz = handler.javaClass + clazz.methods.forEach { method -> + log?.debug("try mapping {}", method) + method.parameterTypes.let { + if (!(it.size == 1 && HttpContent::class.java.isAssignableFrom(it[0])) && it.isNotEmpty()) { + return@forEach } - is GetMapping -> { - routes = annotation.route - router = getRouter("GET") - } - is PostMapping -> { - routes = annotation.route - router = getRouter("POST") - } - is PutMapping -> { - routes = annotation.route - router = getRouter("PUT") - } - is DeleteMapping -> { - routes = annotation.route - router = getRouter("DELETE") - } - is PatchMapping -> { - routes = annotation.route - router = getRouter("PATCH") - } - is TraceMapping -> { - routes = annotation.route - router = getRouter("TRACE") - } - is HeadMapping -> { - routes = annotation.route - router = getRouter("HEAD") - } - is OptionsMapping -> { - routes = annotation.route - router = getRouter("OPTIONS") - } - is ConnectMapping -> { - routes = annotation.route - router = getRouter("CONNECT") - } - else -> return@forEach } + deleteMapping(handler, method) + } + } + + protected fun deleteMapping(obj: Any, method: Method) { + method.annotations.forEach { annotation -> + val (routes, router) = getRoutes(annotation) ?: return@forEach routes.forEach { route -> - log?.info("method route {} mapped to {}", route, method) - router[safeRoute(route)] = if (method.parameterTypes.isEmpty()) when { - method.getAnnotation(Html::class.java) != null -> { content -> - method(obj)?.let { result -> finishHtml(result, content) } - } - method.getAnnotation(Text::class.java) != null -> { content -> - method(obj)?.let { result -> finishText(result, content) } - } - method.getAnnotation(Json::class.java) != null -> { content -> - method(obj)?.let { result -> finishJson(result, content) } - } - else -> { content -> - method(obj)?.let { result -> autoReturn(result, content) } - } - } else { content -> - method(obj, content) + log?.info("delete route {} mapped to {}", route, method) + val handler = router[safeRoute(route)].first + if (handler?.first == obj) { + router.delRoute(safeRoute(route)) } } } } - protected fun getRouter(method: String): Router<(HttpContent) -> Unit> = when { + protected fun getRoutes(annotation: Annotation) = when (annotation) { + is Mapping -> { + annotation.route to getRouter(annotation.method) + } + is GetMapping -> { + annotation.route to getRouter("GET") + } + is PostMapping -> { + annotation.route to getRouter("POST") + } + is PutMapping -> { + annotation.route to getRouter("PUT") + } + is DeleteMapping -> { + annotation.route to getRouter("DELETE") + } + is PatchMapping -> { + annotation.route to getRouter("PATCH") + } + is TraceMapping -> { + annotation.route to getRouter("TRACE") + } + is HeadMapping -> { + annotation.route to getRouter("HEAD") + } + is OptionsMapping -> { + annotation.route to getRouter("OPTIONS") + } + is ConnectMapping -> { + annotation.route to getRouter("CONNECT") + } + else -> null + } + + protected fun getRouter(method: String): Router Unit>> = when { method.isEmpty() -> router else -> { val upperCaseMethod = method.toUpperCase() @@ -191,9 +231,12 @@ open class RoutedHttpHandler( null } - private fun safeRoute(route: String) = if (route.first() == '/') route else "/$route" + fun safeRoute(route: String) = ( + if (route.startsWith('/')) route else "/$route").let { + if (it.endsWith('/')) it.dropLast(1) else it + } - private fun autoReturn(result: Any, content: HttpContent) { + fun autoReturn(result: Any, content: HttpContent) { log?.debug("{}: autoReturn: {}", content.clientIp, result) when (result) { is String -> content.finishText(result.toByteArray()) @@ -210,7 +253,7 @@ open class RoutedHttpHandler( } } - private fun finishHtml(result: Any, content: HttpContent) { + fun finishHtml(result: Any, content: HttpContent) { log?.debug("{}: finishHtml: {}", content.clientIp, result) when (result) { is ByteBuffer -> content.finishHtml(result) @@ -220,7 +263,7 @@ open class RoutedHttpHandler( } } - private fun finishText(result: Any, content: HttpContent) { + fun finishText(result: Any, content: HttpContent) { log?.debug("{}: finishText: {}", content.clientIp, result) when (result) { is ByteBuffer -> content.finishText(result) @@ -230,7 +273,7 @@ open class RoutedHttpHandler( } } - private fun finishJson(result: Any, content: HttpContent) { + fun finishJson(result: Any, content: HttpContent) { log?.debug("{}: finishJson: {}", content.clientIp, result) when (result) { is ByteBuffer -> content.finishJson(result) diff --git a/web/web-coroutine/src/main/kotlin/cn/tursom/web/router/AsyncRoutedHttpHandler.kt b/web/web-coroutine/src/main/kotlin/cn/tursom/web/router/AsyncRoutedHttpHandler.kt new file mode 100644 index 0000000..a15feea --- /dev/null +++ b/web/web-coroutine/src/main/kotlin/cn/tursom/web/router/AsyncRoutedHttpHandler.kt @@ -0,0 +1,182 @@ +package cn.tursom.web.router + +import cn.tursom.web.HttpContent +import cn.tursom.web.MutableHttpContent +import cn.tursom.web.mapping.* +import cn.tursom.web.result.Html +import cn.tursom.web.result.Json +import cn.tursom.web.result.Text +import cn.tursom.web.router.impl.SimpleRouter +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.slf4j.LoggerFactory +import kotlin.reflect.KCallable +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.jvm.jvmErasure + +@Suppress("ProtectedInFinal", "unused", "MemberVisibilityCanBePrivate") +open class AsyncRoutedHttpHandler( + target: Any? = null, + routerMaker: () -> Router Unit>> = { SimpleRouter() }, + val asyncRouterMaker: () -> Router Unit>> = { SimpleRouter() } +) : RoutedHttpHandler(target, routerMaker) { + protected val asyncRouter: Router Unit>> = asyncRouterMaker() + protected val asyncRouterMap: HashMap Unit>>> = HashMap() + + override fun handle(content: HttpContent) { + if (content is MutableHttpContent) { + val handler = getAsyncHandler(content, content.method, content.uri) + if (handler != null) GlobalScope.launch { + handle(content, handler) + } else { + handle(content, getHandler(content, content.method, content.uri)) + } + } else { + val handler = getAsyncHandler(content.method, content.uri).first?.second + if (handler != null) GlobalScope.launch { + handle(content, handler) + } else { + handle(content, getHandler(content.method, content.uri).first?.second) + } + } + } + + open suspend fun handle(content: HttpContent, handler: (suspend (HttpContent) -> Unit)?) { + if (handler != null) { + handler(content) + } else { + notFound(content) + } + } + + fun getAsyncHandler(method: String, route: String): Pair Unit>?, List>> { + val safeRoute = safeRoute(route) + val router = getAsyncRouter(method)[safeRoute] + return if (router.first != null) router else this.asyncRouter[safeRoute] + } + + fun getAsyncHandler(content: MutableHttpContent, method: String, route: String): (suspend (HttpContent) -> Unit)? { + val safeRoute = safeRoute(route) + val router = getAsyncHandler(method, safeRoute) + if (router.first != null) { + router.second.forEach { (k, v) -> + content.addParam(k, v) + } + } + return router.first?.second ?: this.asyncRouter[safeRoute].first?.second + } + + override fun addRouter(handler: Any) { + super.addRouter(handler) + handler::class.members.forEach { member -> + if (member.isSuspend) { + member.parameters.let { + if (it.size != 1 && !(it.size == 2 && HttpContent::class.java.isAssignableFrom(it[1].type.jvmErasure.java))) { + return@forEach + } + } + insertMapping(handler, member) + } + } + } + + @Suppress("UNCHECKED_CAST") + protected fun insertMapping(obj: Any, method: KCallable<*>) { + method.annotations.forEach { annotation -> + log?.info("method route {} annotation {}", method, annotation) + val (routes, router) = getAsyncRoutes(annotation) ?: return@forEach + log?.info("method route {} mapped to {}", method, routes) + routes.forEach { route -> + router[safeRoute(route)] = if (method.parameters.size == 1) { + obj to when { + method.findAnnotation() != null -> { content -> + (method as suspend Any.() -> Any?)(obj)?.let { result -> finishHtml(result, content) } + } + method.findAnnotation() != null -> { content -> + (method as suspend Any.() -> Any?)(obj)?.let { result -> finishText(result, content) } + } + method.findAnnotation() != null -> { content -> + (method as suspend Any.() -> Any?)(obj)?.let { result -> finishJson(result, content) } + } + else -> { content -> + (method as suspend Any.() -> Any?)(obj)?.let { result -> autoReturn(result, content) } + } + } + } else obj to when (method.returnType) { + Void::class.java -> method as suspend (HttpContent) -> Unit + Void.TYPE -> method as suspend (HttpContent) -> Unit + Unit::class.java -> method as suspend (HttpContent) -> Unit + else -> when { + method.findAnnotation() != null -> { content -> + (method as suspend Any.(HttpContent) -> Any?)(obj, content)?.let { result -> finishHtml(result, content) } + } + method.findAnnotation() != null -> { content -> + (method as suspend Any.(HttpContent) -> Any?)(obj, content)?.let { result -> finishText(result, content) } + } + method.findAnnotation() != null -> { content -> + (method as suspend Any.(HttpContent) -> Any?)(obj, content)?.let { result -> finishJson(result, content) } + } + else -> { content -> + (method as suspend Any.(HttpContent) -> Any?)(obj, content)?.let { result -> autoReturn(result, content) } + } + } + } + } + } + } + + protected fun getAsyncRoutes(annotation: Annotation) = when (annotation) { + is Mapping -> { + annotation.route to getAsyncRouter(annotation.method) + } + is GetMapping -> { + annotation.route to getAsyncRouter("GET") + } + is PostMapping -> { + annotation.route to getAsyncRouter("POST") + } + is PutMapping -> { + annotation.route to getAsyncRouter("PUT") + } + is DeleteMapping -> { + annotation.route to getAsyncRouter("DELETE") + } + is PatchMapping -> { + annotation.route to getAsyncRouter("PATCH") + } + is TraceMapping -> { + annotation.route to getAsyncRouter("TRACE") + } + is HeadMapping -> { + annotation.route to getAsyncRouter("HEAD") + } + is OptionsMapping -> { + annotation.route to getAsyncRouter("OPTIONS") + } + is ConnectMapping -> { + annotation.route to getAsyncRouter("CONNECT") + } + else -> null + } + + protected fun getAsyncRouter(method: String): Router Unit>> = when { + method.isEmpty() -> asyncRouter + else -> { + val upperCaseMethod = method.toUpperCase() + var router = asyncRouterMap[upperCaseMethod] + if (router == null) { + router = asyncRouterMaker() + asyncRouterMap[upperCaseMethod] = router + } + router + } + } + + companion object { + private val log = try { + LoggerFactory.getLogger(RoutedHttpHandler::class.java) + } catch (e: Throwable) { + null + } + } +} \ No newline at end of file