Skip to content

Commit ee7325e

Browse files
committed
Use Exception(message) constructor for copying if possible
The code is also refactored a bit to avoid repetition and long lines Fixes #987
1 parent 4764e98 commit ee7325e

File tree

2 files changed

+53
-23
lines changed

2 files changed

+53
-23
lines changed

kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstuctor.kt

+36-23
Original file line numberDiff line numberDiff line change
@@ -12,52 +12,65 @@ import kotlin.concurrent.*
1212

1313
private val throwableFields = Throwable::class.java.fieldsCountOrDefault(-1)
1414
private val cacheLock = ReentrantReadWriteLock()
15+
private typealias Ctor = (Throwable) -> Throwable?
1516
// Replace it with ClassValue when Java 6 support is over
16-
private val exceptionConstructors: WeakHashMap<Class<out Throwable>, (Throwable) -> Throwable?> = WeakHashMap()
17+
private val exceptionCtors: WeakHashMap<Class<out Throwable>, Ctor> = WeakHashMap()
1718

1819
@Suppress("UNCHECKED_CAST")
1920
internal fun <E : Throwable> tryCopyException(exception: E): E? {
21+
// Fast path for CopyableThrowable
2022
if (exception is CopyableThrowable<*>) {
2123
return runCatching { exception.createCopy() as E }.getOrNull()
2224
}
23-
24-
val cachedCtor = cacheLock.read {
25-
exceptionConstructors[exception.javaClass]
25+
// Use cached ctor if found
26+
cacheLock.read { exceptionCtors[exception.javaClass] }?.let { cachedCtor ->
27+
return cachedCtor(exception) as E?
2628
}
27-
28-
if (cachedCtor != null) return cachedCtor(exception) as E?
2929
/*
3030
* Skip reflective copy if an exception has additional fields (that are usually populated in user-defined constructors)
3131
*/
3232
if (throwableFields != exception.javaClass.fieldsCountOrDefault(0)) {
33-
cacheLock.write { exceptionConstructors[exception.javaClass] = { null } }
33+
cacheLock.write { exceptionCtors[exception.javaClass] = { null } }
3434
return null
3535
}
36-
3736
/*
38-
* Try to reflectively find constructor(), constructor(message, cause) or constructor(cause).
37+
* Try to reflectively find constructor(), constructor(message, cause), constructor(cause) or constructor(message).
3938
* Exceptions are shared among coroutines, so we should copy exception before recovering current stacktrace.
4039
*/
41-
var ctor: ((Throwable) -> Throwable?)? = null
40+
var ctor: Ctor? = null
4241
val constructors = exception.javaClass.constructors.sortedByDescending { it.parameterTypes.size }
4342
for (constructor in constructors) {
44-
val parameters = constructor.parameterTypes
45-
if (parameters.size == 2 && parameters[0] == String::class.java && parameters[1] == Throwable::class.java) {
46-
ctor = { e -> runCatching { constructor.newInstance(e.message, e) as E }.getOrNull() }
47-
break
48-
} else if (parameters.size == 1 && parameters[0] == Throwable::class.java) {
49-
ctor = { e -> runCatching { constructor.newInstance(e) as E }.getOrNull() }
50-
break
51-
} else if (parameters.isEmpty()) {
52-
ctor = { e -> runCatching { (constructor.newInstance() as E).also { it.initCause(e) } }.getOrNull() }
53-
break
54-
}
43+
ctor = createConstructor(constructor)
44+
if (ctor != null) break
5545
}
56-
57-
cacheLock.write { exceptionConstructors[exception.javaClass] = (ctor ?: { null }) }
46+
// Store the resulting ctor to cache
47+
cacheLock.write { exceptionCtors[exception.javaClass] = ctor ?: { null } }
5848
return ctor?.invoke(exception) as E?
5949
}
6050

51+
private fun createConstructor(constructor: Constructor<*>): Ctor? {
52+
val p = constructor.parameterTypes
53+
return when (p.size) {
54+
2 -> when {
55+
p[0] == String::class.java && p[1] == Throwable::class.java ->
56+
safeCtor { e -> constructor.newInstance(e.message, e) as Throwable }
57+
else -> null
58+
}
59+
1 -> when (p[0]) {
60+
Throwable::class.java ->
61+
safeCtor { e -> constructor.newInstance(e) as Throwable }
62+
String::class.java ->
63+
safeCtor { e -> (constructor.newInstance(e.message) as Throwable).also { it.initCause(e) } }
64+
else -> null
65+
}
66+
0 -> safeCtor { e -> (constructor.newInstance() as Throwable).also { it.initCause(e) } }
67+
else -> null
68+
}
69+
}
70+
71+
private inline fun safeCtor(crossinline block: (Throwable) -> Throwable): Ctor =
72+
{ e -> runCatching { block(e) }.getOrNull() }
73+
6174
private fun Class<*>.fieldsCountOrDefault(defaultValue: Int) = kotlin.runCatching { fieldsCount() }.getOrDefault(defaultValue)
6275

6376
private tailrec fun Class<*>.fieldsCount(accumulator: Int = 0): Int {

kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryCustomExceptionsTest.kt

+17
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,21 @@ class StackTraceRecoveryCustomExceptionsTest : TestBase() {
5454
assertEquals(239, cause.customData)
5555
}
5656
}
57+
58+
internal class WithDefault(message: String = "default") : Exception(message)
59+
60+
@Test
61+
fun testStackTraceRecoveredWithCustomMessage() = runTest {
62+
try {
63+
withContext(wrapperDispatcher(coroutineContext)) {
64+
throw WithDefault("custom")
65+
}
66+
expectUnreached()
67+
} catch (e: WithDefault) {
68+
assertEquals("custom", e.message)
69+
val cause = e.cause
70+
assertTrue(cause is WithDefault)
71+
assertEquals("custom", cause.message)
72+
}
73+
}
5774
}

0 commit comments

Comments
 (0)