使用 Kotlin 协程创建动态代理

Posted 码农乐园

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用 Kotlin 协程创建动态代理相关的知识,希望对你有一定的参考价值。

够在运行时实现接口,并决定如何在调用方法时动态执行方法。这对于在装饰器模式中描述的现有实现(尤其是来自第三方库)周围添加附加功能非常有用。然而,Kotlin协程在创建动态代理时引入了一系列新问题。我们将探讨如何正确检测和动态调用在接口上声明的挂起函数。

设置

首先,使用 Gradle 创建一个简单的 Kotlin 控制台应用程序并添加以下依赖项。

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3")
ITest
interface ITest 
    fun test(): String?
    @Throws(Exception::class)
    fun testException(): String?
    suspend fun testSuspend(): String?
    @Throws(Exception::class)
    suspend fun testExceptionSuspend(): String?
ITest
class TestImpl: ITest 
    override fun test(): String? 
        return "test result"
    

    @Throws(Exception::class)
    override fun testException(): String? 
        throw Exception("You called testException().")
    

    override suspend fun testSuspend(): String? 
        delay(1000)
        return "testSuspend result"
    

    @Throws(Exception::class)
    override suspend fun testExceptionSuspend(): String? 
        delay(1000)
        throw Exception("You called testExceptionSuspend().")
    

目标

WrappedTestException
class WrappedTestException(cause: Throwable): Exception(cause)

缺乏经验的方法

InvocationTargetExceptionWrappedTestException
/**
 * File: main.kt
 *
 * Dynamic proxy that naively catches and prints out exceptions and re-throws them wrapped as WrappedTestExceptions.
 */
class NaiveExceptionLogger<T>(private val instance: T): InvocationHandler 
    override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? 
        try 
            val nonNullArgs = args ?: arrayOf()
            return method?.invoke(instance, *nonNullArgs)
         catch(e: InvocationTargetException) 
            e.targetException?.let targetException ->
                println("Naively caught underlying target exception $targetException")
                throw WrappedTestException(targetException)
             ?: throw WrappedTestException(e)
        
    
TestFactorymainrunBlockingmain
object TestFactory 
    fun createNaively(): ITest 
        return Proxy.newProxyInstance(
            ITest::class.java.classLoader,
            arrayOf<Class<*>>(ITest::class.java),
            NaiveExceptionLogger(TestImpl())) as ITest
    



fun main(args: Array<String>) 
    runBlocking 
        val test = TestFactory.createNaively()
        println(test.test())
        println(test.testSuspend())
        try 
            test.testException()
            throw IllegalStateException("Did not catch testException()")
         catch(e: WrappedTestException)  
        try 
            test.testExceptionSuspend()
            throw IllegalStateException("Did not catch testExceptionSuspend()")
         catch(e: WrappedTestException)  
    

当你运行它时,你应该得到以下输出:

test result
testSuspend result
Naively caught underlying target exception java.lang.Exception: You called testException().
Exception in thread "main" java.lang.Exception: You called testExceptionSuspend().
    at TestImpl.testExceptionSuspend(main.kt:39)
    at TestImpl$testExceptionSuspend$1.invokeSuspend(main.kt)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
    at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
    at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:369)
    at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:403)
    at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:395)
    at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:491)
    at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:489)
    at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
    at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:84)
    at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
    at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
    at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
    at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
    at MainKt.main(main.kt:126)

Process finished with exit code 1
testExceptionSuspendNaiveExceptionLogger

正确的方法

testExceptionSuspend
resumeWithWrappedTestExceptionCoroutineScope

我们也可以根据自己的需求来组合这个上下文,例如通过 Dispatchers启动在IO 线程池中运行的调用。调用该方法时,我们现在只需要更改与我们的新的和改进的 Continuation 一起提供的最后一个参数。

既然我们已经在 IO 线程池中启动了方法调用,那么我们从动态调用的方法中返回什么?Kotlin 对挂起函数有一个神奇的返回值,它告诉运行时挂起执行并允许当前线程继续执行其他工作。它是 kotlin.coroutines.intrinsics。COROUTINE_SUSPENDED。 

这是所有东西放在一起时的样子:

/**
 * File: main.kt
 *
 * Dynamic proxy that correctly catches and prints out exceptions with proper handling for coroutines.
 * Rethrows caught exceptions as WrappedTestException
 */
class CorrectExceptionLogger<T>(private val instance: T): InvocationHandler 
    override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? 
        val nonNullArgs = args ?: arrayOf()
        try 
            val lastArg = nonNullArgs.lastOrNull()
            if(lastArg == null || lastArg !is Continuation<*>) 
                // not a suspend func, just invoke regularly
                return method?.invoke(instance, *nonNullArgs)
             else 
                // Step 1: Wrap the underlying continuation to intercept exceptions.
                @Suppress("UNCHECKED_CAST")
                val originalContinuation = lastArg as Continuation<Any?>
                val wrappedContinuation = object: Continuation<Any?> 
                    override val context: CoroutineContext get() = originalContinuation.context
                    override fun resumeWith(result: Result<Any?>) 
                        result.exceptionOrNull()?.let err ->
                            // Step 2: log intercepted exception and resume with our custom wrapped exception.
                            println("Correctly caught underlying coroutine exception $err")
                            originalContinuation.resumeWithException(WrappedTestException(err))
                         ?: originalContinuation.resumeWith(result)
                    
                
                // Step 3: launch the suspend function with our wrapped continuation using the underlying scope and context, but force it to run in the IO thread pool
                CoroutineScope(originalContinuation.context).launch(Dispatchers.IO + originalContinuation.context) 
                    val argumentsWithoutContinuation = nonNullArgs.take(nonNullArgs.size - 1)
                    val newArgs = argumentsWithoutContinuation + wrappedContinuation
                    method?.invoke(instance, *newArgs.toTypedArray())
                
                return kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
            
         catch(e: InvocationTargetException) 
            e.targetException?.let targetException ->
                println("Correctly caught underlying exception $targetException")
                throw WrappedTestException(targetException)
             ?: throw WrappedTestException(e)
        
    

运行!

TestFactory
object TestFactory 
    fun createNaively(): ITest 
        return Proxy.newProxyInstance(
            ITest::class.java.classLoader,
            arrayOf<Class<*>>(ITest::class.java),
            NaiveExceptionLogger(TestImpl())) as ITest
    

    fun createCorrectly(): ITest 
        return Proxy.newProxyInstance(
            ITest::class.java.classLoader,
            arrayOf<Class<*>>(ITest::class.java),
            CorrectExceptionLogger(TestImpl())) as ITest
    



fun main(args: Array<String>) 
    runBlocking 
        val test = TestFactory.createCorrectly()
        println(test.test())
        println(test.testSuspend())
        try 
            test.testException()
            throw IllegalStateException("Did not catch testException()")
         catch(e: WrappedTestException)  
        try 
            test.testExceptionSuspend()
            throw IllegalStateException("Did not catch testExceptionSuspend()")
         catch(e: WrappedTestException)  
    

您的输出应如下所示。如果是这样——恭喜!– 您现在已经创建了一个通用的动态代理,它可以与常规和挂起函数一起使用。

test result
testSuspend result
Correctly caught underlying exception java.lang.Exception: You called testException().
Correctly caught underlying coroutine exception java.lang.Exception: You called testExceptionSuspend().

Process finished with exit code 0

以上是关于使用 Kotlin 协程创建动态代理的主要内容,如果未能解决你的问题,请参考以下文章