From 7525dc7c3366766746eda6991045754803868092 Mon Sep 17 00:00:00 2001 From: Him188 Date: Sun, 10 May 2020 00:41:18 +0800 Subject: [PATCH] Add JvmMethodListeners.kt --- .../net/mamoe/mirai/event/JvmMethodEvents.kt | 26 -- .../mamoe/mirai/event/JvmMethodListeners.kt | 248 ++++++++++++++++++ .../net/mamoe/mirai/event/EventTests.kt | 2 +- .../mamoe/mirai/event/JvmMethodEventsTest.kt | 117 +++++++++ 4 files changed, 366 insertions(+), 27 deletions(-) delete mode 100644 mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/JvmMethodEvents.kt create mode 100644 mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/JvmMethodListeners.kt create mode 100644 mirai-core/src/jvmTest/kotlin/net/mamoe/mirai/event/JvmMethodEventsTest.kt diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/JvmMethodEvents.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/JvmMethodEvents.kt deleted file mode 100644 index 6b9edad0e..000000000 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/JvmMethodEvents.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2020 Mamoe Technologies and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. - * - * https://github.com/mamoe/mirai/blob/master/LICENSE - */ - -package net.mamoe.mirai.event - -/** - * - */ -@Target(AnnotationTarget.FUNCTION) -@Retention(AnnotationRetention.RUNTIME) -annotation class EventHandler( - val priority: Listener.EventPriority = Listener.EventPriority.NORMAL, - val ignoreCancelled: Boolean = true -) - -interface ListenerHoster - -fun ListenerHoster.registerEvents() { - -} \ No newline at end of file diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/JvmMethodListeners.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/JvmMethodListeners.kt new file mode 100644 index 000000000..7abcd480b --- /dev/null +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/JvmMethodListeners.kt @@ -0,0 +1,248 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +@file:JvmName("Events") +@file:Suppress("unused", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "NOTHING_TO_INLINE") + +package net.mamoe.mirai.event + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.lang.reflect.Method +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.reflect.KClass +import kotlin.reflect.full.IllegalCallableAccessException +import kotlin.reflect.full.callSuspend +import kotlin.reflect.full.isSubclassOf +import kotlin.reflect.jvm.isAccessible +import kotlin.reflect.jvm.kotlinFunction + +/** + * 标注一个函数为事件监听器. + * + * ### Kotlin 函数 + * Kotlin 函数要求: + * - 接收者和函数参数: 所标注的 Kotlin 函数必须至少拥有一个接收者或一个函数参数, 或二者都具有. 接收者和函数参数的类型必须相同 (如果二者都有) + * 接收者或函数参数的类型都必须为 [Event] 或其子类. + * - 返回值: 为 [Unit] 或不指定返回值时将注册为 [CoroutineScope.subscribeAlways], 为 [ListeningStatus] 时将注册为 [CoroutineScope.subscribe] + * + * 所有 Kotlin 非 `suspend` 的函数都将会在 [Dispatchers.IO] 中调用 + * + * 所有支持的函数类型: + * ``` + * suspend fun T.onEvent(T) + * suspend fun T.onEvent(T): ListeningStatus + * suspend fun onEvent(T) + * suspend fun onEvent(T): ListeningStatus + * suspend fun T.onEvent() + * suspend fun T.onEvent(): ListeningStatus + * fun T.onEvent(T) + * fun T.onEvent(T): ListeningStatus + * fun onEvent(T) + * fun onEvent(T): ListeningStatus + * fun T.onEvent() + * fun T.onEvent(): ListeningStatus + * ``` + * + * ### Java 方法 + * 所有 Java 方法都会在 [Dispatchers.IO] 中调用. + * + * 支持的方法类型 + * ``` + * void onEvent(T) + * ListeningStatus onEvent(T) + * ``` + * + * @sample net.mamoe.mirai.event.JvmMethodEventsTest + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class EventHandler( + /** + * 监听器优先级 + * @see Listener.EventPriority + * @see Event.intercept + */ + val priority: Listener.EventPriority = Listener.EventPriority.NORMAL, + /** + * 是否自动忽略被 [取消][CancellableEvent.isCancelled] + * @see CancellableEvent + */ + val ignoreCancelled: Boolean = true, + /** + * 并发类型 + * @see Listener.ConcurrencyKind + */ + val concurrency: Listener.ConcurrencyKind = Listener.ConcurrencyKind.CONCURRENT +) + +/** + * 实现这个接口的对象可以通过 [EventHandler] 标注事件监听函数, 并通过 [registerEvents] 注册. + */ +interface ListenerHost + +/** + * 反射得到所有标注了 [EventHandler] 的函数 (Java 为方法), 并注册为事件监听器 + * @see EventHandler 获取更多信息 + */ +@JvmOverloads +fun T.registerEvents(coroutineContext: CoroutineContext = EmptyCoroutineContext) + where T : CoroutineScope, T : ListenerHost = this.registerEvents(this, coroutineContext) + +/** + * 反射得到所有标注了 [EventHandler] 的函数 (Java 为方法), 并注册为事件监听器 + * @see EventHandler 获取更多信息 + */ +@JvmOverloads +fun CoroutineScope.registerEvents(host: ListenerHost, coroutineContext: CoroutineContext = EmptyCoroutineContext) { + for (method in host.javaClass.declaredMethods) { + method.getAnnotation(EventHandler::class.java)?.let { + method.registerEvent(host, this, it, coroutineContext) + } + } +} + + + +@Suppress("UNCHECKED_CAST") +private fun Method.registerEvent( + owner: Any, + scope: CoroutineScope, + annotation: EventHandler, + coroutineContext: CoroutineContext +): Listener { + this.isAccessible = true + val kotlinFunction = this.kotlinFunction + return if (kotlinFunction != null) { + // kotlin functions + + val param = kotlinFunction.parameters + when (param.size) { + 3 -> { // ownerClass, receiver, event + check(param[1].type == param[2].type) { "Illegal kotlin function ${kotlinFunction.name}. Receiver and param must have same type" } + check((param[1].type.classifier as? KClass<*>)?.isSubclassOf(Event::class) == true) { + "Illegal kotlin function ${kotlinFunction.name}. First param or receiver must be subclass of Event, but found ${param[1].type.classifier}" + } + } + 2 -> { // ownerClass, event + check((param[1].type.classifier as? KClass<*>)?.isSubclassOf(Event::class) == true) { + "Illegal kotlin function ${kotlinFunction.name}. First param or receiver must be subclass of Event, but found ${param[1].type.classifier}" + } + } + else -> error("function ${kotlinFunction.name} must have one Event param") + } + lateinit var listener: Listener<*> + kotlinFunction.isAccessible = true + suspend fun callFunction(event: Event): Any? { + try { + return when (param.size) { + 3 -> { + if (kotlinFunction.isSuspend) { + kotlinFunction.callSuspend(owner, event, event) + } else withContext(Dispatchers.IO) { // for safety + kotlinFunction.call(owner, event, event) + } + + } + 2 -> { + if (kotlinFunction.isSuspend) { + kotlinFunction.callSuspend(owner, event) + } else withContext(Dispatchers.IO) { // for safety + kotlinFunction.call(owner, event) + } + } + else -> error("stub") + } + } catch (e: IllegalCallableAccessException) { + listener.completeExceptionally(e) + return ListeningStatus.STOPPED + } + } + when (kotlinFunction.returnType.classifier) { + Unit::class -> { + scope.subscribeAlways( + param[1].type.classifier as KClass, + priority = annotation.priority, + concurrency = annotation.concurrency, + coroutineContext = coroutineContext + ) { + if (annotation.ignoreCancelled) { + if ((this as? CancellableEvent)?.isCancelled != true) { + callFunction(this) + } + } else callFunction(this) + }.also { listener = it } + } + ListeningStatus::class -> { + scope.subscribe( + param[1].type.classifier as KClass, + priority = annotation.priority, + concurrency = annotation.concurrency, + coroutineContext = coroutineContext + ) { + if (annotation.ignoreCancelled) { + if ((this as? CancellableEvent)?.isCancelled != true) { + callFunction(this) as ListeningStatus + } else ListeningStatus.LISTENING + } else callFunction(this) as ListeningStatus + }.also { listener = it } + } + else -> error("Illegal method return type. Required Void or ListeningStatus, found ${kotlinFunction.returnType.classifier}") + } + } else { + // java methods + + val paramType = this.parameters[0].type + check(this.parameterCount == 1 && Event::class.java.isAssignableFrom(paramType)) { + "Illegal method parameter. Required one exact Event subclass. found $paramType" + } + when (this.returnType) { + Void::class.java -> { + scope.subscribeAlways( + paramType.kotlin as KClass, + priority = annotation.priority, + concurrency = annotation.concurrency, + coroutineContext = coroutineContext + ) { + if (annotation.ignoreCancelled) { + if ((this as? CancellableEvent)?.isCancelled != true) { + withContext(Dispatchers.IO) { + this@registerEvent.invoke(owner, this) + } + } + } else withContext(Dispatchers.IO) { + this@registerEvent.invoke(owner, this) + } + } + } + ListeningStatus::class.java -> { + scope.subscribe( + paramType.kotlin as KClass, + priority = annotation.priority, + concurrency = annotation.concurrency, + coroutineContext = coroutineContext + ) { + if (annotation.ignoreCancelled) { + if ((this as? CancellableEvent)?.isCancelled != true) { + withContext(Dispatchers.IO) { + this@registerEvent.invoke(owner, this) as ListeningStatus + } + } else ListeningStatus.LISTENING + } else withContext(Dispatchers.IO) { + this@registerEvent.invoke(owner, this) as ListeningStatus + } + + } + } + else -> error("Illegal method return type. Required Void or ListeningStatus, but found ${this.returnType.canonicalName}") + } + } +} \ No newline at end of file diff --git a/mirai-core/src/jvmTest/kotlin/net/mamoe/mirai/event/EventTests.kt b/mirai-core/src/jvmTest/kotlin/net/mamoe/mirai/event/EventTests.kt index a6cf5cfdf..54d7cebbb 100644 --- a/mirai-core/src/jvmTest/kotlin/net/mamoe/mirai/event/EventTests.kt +++ b/mirai-core/src/jvmTest/kotlin/net/mamoe/mirai/event/EventTests.kt @@ -19,7 +19,7 @@ import java.util.concurrent.atomic.AtomicInteger import kotlin.test.Test import kotlin.test.assertTrue -class TestEvent : Event, AbstractEvent() { +class TestEvent : AbstractEvent() { var triggered = false } diff --git a/mirai-core/src/jvmTest/kotlin/net/mamoe/mirai/event/JvmMethodEventsTest.kt b/mirai-core/src/jvmTest/kotlin/net/mamoe/mirai/event/JvmMethodEventsTest.kt new file mode 100644 index 000000000..2bd0b100b --- /dev/null +++ b/mirai-core/src/jvmTest/kotlin/net/mamoe/mirai/event/JvmMethodEventsTest.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +@file:Suppress("RedundantSuspendModifier", "unused") + +package net.mamoe.mirai.event + +import kotlinx.coroutines.CoroutineScope +import net.mamoe.mirai.utils.internal.runBlocking +import org.junit.Test +import java.util.concurrent.atomic.AtomicInteger +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.test.assertEquals + + +internal class JvmMethodEventsTest { + + @Test + fun testMethodListener() { + class TestClass : ListenerHost, CoroutineScope by CoroutineScope(EmptyCoroutineContext) { + private var called = AtomicInteger(0) + + fun getCalled() = called.get() + + @EventHandler + suspend fun TestEvent.`suspend receiver param Unit`(event: TestEvent) { + called.getAndIncrement() + } + + @EventHandler + suspend fun TestEvent.`suspend receiver Unit`() { + called.getAndIncrement() + } + + @EventHandler + suspend fun `suspend param Unit`(event: TestEvent) { + called.getAndIncrement() + } + + @EventHandler + fun TestEvent.`receiver param Unit`(event: TestEvent) { + called.getAndIncrement() + } + + @EventHandler + suspend fun TestEvent.`suspend receiver param LS`(event: TestEvent): ListeningStatus { + called.getAndIncrement() + return ListeningStatus.STOPPED + } + + @EventHandler + suspend fun TestEvent.`suspend receiver LS`(): ListeningStatus { + called.getAndIncrement() + return ListeningStatus.STOPPED + } + + @EventHandler + suspend fun `suspend param LS`(event: TestEvent): ListeningStatus { + called.getAndIncrement() + return ListeningStatus.STOPPED + } + + @EventHandler + fun TestEvent.`receiver param LS`(event: TestEvent): ListeningStatus { + called.getAndIncrement() + return ListeningStatus.STOPPED + } + } + + TestClass().run { + this.registerEvents() + + runBlocking { + TestEvent().broadcast() + } + + assertEquals(8, this.getCalled()) + } + } + + @Test + fun testIntercept() { + class TestClass : ListenerHost, CoroutineScope by CoroutineScope(EmptyCoroutineContext) { + private var called = AtomicInteger(0) + + fun getCalled() = called.get() + + @EventHandler(Listener.EventPriority.HIGHEST) + private suspend fun TestEvent.`suspend receiver param Unit`(event: TestEvent) { + intercept() + called.getAndIncrement() + } + + @EventHandler(Listener.EventPriority.MONITOR) + private fun TestEvent.`receiver param LS`(event: TestEvent): ListeningStatus { + called.getAndIncrement() + return ListeningStatus.STOPPED + } + } + + TestClass().run { + this.registerEvents() + + runBlocking { + TestEvent().broadcast() + } + + assertEquals(1, this.getCalled()) + } + } +}