Skip to content

Commit c718419

Browse files
authored
KTOR-7298: Load modules dynamically only if development mode is enabled (#4715)
* Make the module list immutable after ServerConfig creation * Handle cases when lambda has more or less than one constructor
1 parent f02140d commit c718419

File tree

4 files changed

+95
-43
lines changed

4 files changed

+95
-43
lines changed

ktor-server/ktor-server-core/common/src/io/ktor/server/application/Application.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2019 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package io.ktor.server.application
@@ -9,8 +9,11 @@ import io.ktor.server.engine.*
99
import io.ktor.util.*
1010
import io.ktor.util.logging.*
1111
import io.ktor.utils.io.*
12-
import kotlinx.coroutines.*
13-
import kotlin.coroutines.*
12+
import kotlinx.coroutines.CoroutineScope
13+
import kotlinx.coroutines.Job
14+
import kotlinx.coroutines.SupervisorJob
15+
import kotlin.coroutines.CoroutineContext
16+
import kotlin.coroutines.EmptyCoroutineContext
1417

1518
/**
1619
* A builder for [ServerConfig].
@@ -61,7 +64,7 @@ public class ServerConfigBuilder(
6164
}
6265

6366
internal fun build(): ServerConfig =
64-
ServerConfig(environment, modules, watchPaths, rootPath, developmentMode, parentCoroutineContext)
67+
ServerConfig(environment, modules.toList(), watchPaths, rootPath, developmentMode, parentCoroutineContext)
6568
}
6669

6770
/**
@@ -72,7 +75,7 @@ public class ServerConfigBuilder(
7275
*/
7376
public class ServerConfig internal constructor(
7477
public val environment: ApplicationEnvironment,
75-
internal val modules: MutableList<Application.() -> Unit>,
78+
internal val modules: List<Application.() -> Unit>,
7679
internal val watchPaths: List<String>,
7780
public val rootPath: String,
7881
public val developmentMode: Boolean = PlatformUtils.IS_DEVELOPMENT_MODE,

ktor-server/ktor-server-core/jvm/src/io/ktor/server/engine/EmbeddedServerJvm.kt

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package io.ktor.server.engine
@@ -14,15 +14,24 @@ import io.ktor.util.logging.*
1414
import io.ktor.util.pipeline.*
1515
import io.ktor.utils.io.*
1616
import io.ktor.utils.io.core.*
17-
import kotlinx.coroutines.*
18-
import java.io.*
19-
import java.net.*
17+
import kotlinx.coroutines.CoroutineScope
18+
import kotlinx.coroutines.Dispatchers
19+
import kotlinx.coroutines.launch
20+
import kotlinx.coroutines.withContext
21+
import java.io.File
22+
import java.net.URL
23+
import java.net.URLDecoder
2024
import java.nio.file.*
2125
import java.nio.file.StandardWatchEventKinds.*
22-
import java.nio.file.attribute.*
23-
import java.util.concurrent.*
24-
import java.util.concurrent.locks.*
25-
import kotlin.concurrent.*
26+
import java.nio.file.attribute.BasicFileAttributes
27+
import java.util.concurrent.TimeUnit
28+
import java.util.concurrent.locks.ReentrantReadWriteLock
29+
import kotlin.concurrent.getOrSet
30+
import kotlin.concurrent.read
31+
import kotlin.concurrent.write
32+
33+
private typealias ApplicationModule = Application.() -> Unit
34+
private typealias DynamicApplicationModule = Application.(ClassLoader) -> Unit
2635

2736
public actual class EmbeddedServer<
2837
TEngine : ApplicationEngine,
@@ -34,6 +43,7 @@ actual constructor(
3443
engineConfigBlock: TConfiguration.() -> Unit
3544
) {
3645

46+
@Suppress("DEPRECATION")
3747
public actual val monitor: Events = rootConfig.environment.monitor
3848

3949
public actual val environment: ApplicationEnvironment = rootConfig.environment
@@ -50,11 +60,13 @@ actual constructor(
5060
private val configuredWatchPath = environment.config.propertyOrNull("ktor.deployment.watch")?.getList().orEmpty()
5161
private val watchPatterns: List<String> = configuredWatchPath + rootConfig.watchPaths
5262

53-
private val configModulesNames: List<String> = run {
54-
environment.config.propertyOrNull("ktor.application.modules")?.getList() ?: emptyList()
55-
}
63+
private val configModulesNames: List<String> =
64+
environment.config.propertyOrNull("ktor.application.modules")?.getList().orEmpty()
5665

57-
private val modulesNames: List<String> = configModulesNames
66+
private val modules by lazy {
67+
configModulesNames.map(::dynamicModule) +
68+
rootConfig.modules.map { module -> module.toDynamicModuleOrNull() ?: module.wrapWithDynamicModule() }
69+
}
5870

5971
private var applicationInstance: Application? = Application(
6072
environment,
@@ -356,23 +368,53 @@ actual constructor(
356368
safeRaiseEvent(ApplicationStarting, newInstance)
357369

358370
avoidingDoubleStartup {
359-
modulesNames.forEach { name ->
360-
launchModuleByName(name, currentClassLoader, newInstance)
361-
}
371+
modules.forEach { module -> module(newInstance, currentClassLoader) }
372+
}
362373

363-
rootConfig.modules.forEach { module ->
364-
val name = module.methodName()
374+
safeRaiseEvent(ApplicationStarted, newInstance)
375+
return newInstance
376+
}
365377

366-
try {
367-
launchModuleByName(name, currentClassLoader, newInstance)
368-
} catch (_: ReloadingException) {
369-
module(newInstance)
370-
}
378+
private fun dynamicModule(name: String): DynamicApplicationModule {
379+
return { classLoader ->
380+
val application = this
381+
launchModuleByName(name, classLoader, application)
382+
}
383+
}
384+
385+
private fun ApplicationModule.toDynamicModuleOrNull(): DynamicApplicationModule? {
386+
// Programmatic modules are loaded dynamically only when development mode is active
387+
if (!rootConfig.developmentMode) return null
388+
389+
val module = this
390+
// Method name getting might fail if method signature has been changed after compilation
391+
// (for example by R8 or ProGuard)
392+
val name = runCatching { module.methodName() }
393+
.onFailure { cause ->
394+
environment.log.debug(
395+
"Module can't be loaded dynamically, auto-reloading won't work for this module",
396+
cause,
397+
)
398+
}
399+
.getOrElse { return null }
400+
401+
return { classLoader ->
402+
val application = this
403+
try {
404+
launchModuleByName(name, classLoader, application)
405+
} catch (cause: ReloadingException) {
406+
environment.log.debug(
407+
"Failed to load module '$name' by classpath reference, falling back to currently loaded value",
408+
cause,
409+
)
410+
module.invoke(application)
371411
}
372412
}
413+
}
373414

374-
safeRaiseEvent(ApplicationStarted, newInstance)
375-
return newInstance
415+
private fun ApplicationModule.wrapWithDynamicModule(): DynamicApplicationModule {
416+
val module = this
417+
return { module() }
376418
}
377419

378420
private fun launchModuleByName(name: String, currentClassLoader: ClassLoader, newInstance: Application) {
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
/*
2-
* Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3-
*/
2+
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
44

55
package io.ktor.server.engine
66

7-
import kotlin.reflect.*
8-
import kotlin.reflect.jvm.*
7+
import kotlin.reflect.KFunction
8+
import kotlin.reflect.jvm.javaMethod
99

1010
/**
1111
* Obtain function FQName.
1212
*/
1313
internal fun Function<*>.methodName(): String {
1414
val method = (this as? KFunction<*>)?.javaMethod ?: return "${javaClass.name}.invoke"
1515

16-
val clazz = method.declaringClass
16+
val className = method.declaringClass.name
1717
val name = method.name
18-
return "${clazz.name}.$name"
18+
return "$className.$name"
1919
}

ktor-server/ktor-server-core/jvm/src/io/ktor/server/engine/internal/CallableUtils.kt

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
/*
2-
* Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package io.ktor.server.engine.internal
66

77
import io.ktor.server.application.*
8-
import java.lang.reflect.*
9-
import kotlin.reflect.*
10-
import kotlin.reflect.full.*
11-
import kotlin.reflect.jvm.*
8+
import java.lang.reflect.InvocationTargetException
9+
import java.lang.reflect.Modifier
10+
import kotlin.reflect.KClass
11+
import kotlin.reflect.KFunction
12+
import kotlin.reflect.KParameter
13+
import kotlin.reflect.full.functions
14+
import kotlin.reflect.jvm.javaType
15+
import kotlin.reflect.jvm.kotlinFunction
1216

1317
internal fun executeModuleFunction(
1418
classLoader: ClassLoader,
@@ -40,10 +44,13 @@ internal fun executeModuleFunction(
4044

4145
try {
4246
if (Function1::class.java.isAssignableFrom(clazz)) {
43-
val constructor = clazz.declaredConstructors.single()
44-
if (constructor.parameterCount != 0) {
45-
throw ReloadingException("Module function with captured variables cannot be instantiated '$fqName'")
47+
// Normally lambda has a single constructor, but this could change after R8/ProGuard optimizations
48+
val constructors = clazz.declaredConstructors
49+
if (constructors.isEmpty()) {
50+
throw ReloadingException("Module function cannot be instantiated '$fqName'")
4651
}
52+
val constructor = constructors.find { it.parameterCount == 0 }
53+
?: throw ReloadingException("Module function with captured variables cannot be instantiated '$fqName'")
4754

4855
constructor.isAccessible = true
4956
@Suppress("UNCHECKED_CAST")

0 commit comments

Comments
 (0)