Skip to content

Commit

Permalink
KTOR-7298: Load modules dynamically only if development mode is enabl…
Browse files Browse the repository at this point in the history
…ed (#4715)

* Make the module list immutable after ServerConfig creation
* Handle cases when lambda has more or less than one constructor
  • Loading branch information
osipxd authored Feb 28, 2025
1 parent f02140d commit c718419
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 43 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.server.application
Expand All @@ -9,8 +9,11 @@ import io.ktor.server.engine.*
import io.ktor.util.*
import io.ktor.util.logging.*
import io.ktor.utils.io.*
import kotlinx.coroutines.*
import kotlin.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

/**
* A builder for [ServerConfig].
Expand Down Expand Up @@ -61,7 +64,7 @@ public class ServerConfigBuilder(
}

internal fun build(): ServerConfig =
ServerConfig(environment, modules, watchPaths, rootPath, developmentMode, parentCoroutineContext)
ServerConfig(environment, modules.toList(), watchPaths, rootPath, developmentMode, parentCoroutineContext)
}

/**
Expand All @@ -72,7 +75,7 @@ public class ServerConfigBuilder(
*/
public class ServerConfig internal constructor(
public val environment: ApplicationEnvironment,
internal val modules: MutableList<Application.() -> Unit>,
internal val modules: List<Application.() -> Unit>,
internal val watchPaths: List<String>,
public val rootPath: String,
public val developmentMode: Boolean = PlatformUtils.IS_DEVELOPMENT_MODE,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.server.engine
Expand All @@ -14,15 +14,24 @@ import io.ktor.util.logging.*
import io.ktor.util.pipeline.*
import io.ktor.utils.io.*
import io.ktor.utils.io.core.*
import kotlinx.coroutines.*
import java.io.*
import java.net.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.net.URL
import java.net.URLDecoder
import java.nio.file.*
import java.nio.file.StandardWatchEventKinds.*
import java.nio.file.attribute.*
import java.util.concurrent.*
import java.util.concurrent.locks.*
import kotlin.concurrent.*
import java.nio.file.attribute.BasicFileAttributes
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.getOrSet
import kotlin.concurrent.read
import kotlin.concurrent.write

private typealias ApplicationModule = Application.() -> Unit
private typealias DynamicApplicationModule = Application.(ClassLoader) -> Unit

public actual class EmbeddedServer<
TEngine : ApplicationEngine,
Expand All @@ -34,6 +43,7 @@ actual constructor(
engineConfigBlock: TConfiguration.() -> Unit
) {

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

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

private val configModulesNames: List<String> = run {
environment.config.propertyOrNull("ktor.application.modules")?.getList() ?: emptyList()
}
private val configModulesNames: List<String> =
environment.config.propertyOrNull("ktor.application.modules")?.getList().orEmpty()

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

private var applicationInstance: Application? = Application(
environment,
Expand Down Expand Up @@ -356,23 +368,53 @@ actual constructor(
safeRaiseEvent(ApplicationStarting, newInstance)

avoidingDoubleStartup {
modulesNames.forEach { name ->
launchModuleByName(name, currentClassLoader, newInstance)
}
modules.forEach { module -> module(newInstance, currentClassLoader) }
}

rootConfig.modules.forEach { module ->
val name = module.methodName()
safeRaiseEvent(ApplicationStarted, newInstance)
return newInstance
}

try {
launchModuleByName(name, currentClassLoader, newInstance)
} catch (_: ReloadingException) {
module(newInstance)
}
private fun dynamicModule(name: String): DynamicApplicationModule {
return { classLoader ->
val application = this
launchModuleByName(name, classLoader, application)
}
}

private fun ApplicationModule.toDynamicModuleOrNull(): DynamicApplicationModule? {
// Programmatic modules are loaded dynamically only when development mode is active
if (!rootConfig.developmentMode) return null

val module = this
// Method name getting might fail if method signature has been changed after compilation
// (for example by R8 or ProGuard)
val name = runCatching { module.methodName() }
.onFailure { cause ->
environment.log.debug(
"Module can't be loaded dynamically, auto-reloading won't work for this module",
cause,
)
}
.getOrElse { return null }

return { classLoader ->
val application = this
try {
launchModuleByName(name, classLoader, application)
} catch (cause: ReloadingException) {
environment.log.debug(
"Failed to load module '$name' by classpath reference, falling back to currently loaded value",
cause,
)
module.invoke(application)
}
}
}

safeRaiseEvent(ApplicationStarted, newInstance)
return newInstance
private fun ApplicationModule.wrapWithDynamicModule(): DynamicApplicationModule {
val module = this
return { module() }
}

private fun launchModuleByName(name: String, currentClassLoader: ClassLoader, newInstance: Application) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
/*
* Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.server.engine

import kotlin.reflect.*
import kotlin.reflect.jvm.*
import kotlin.reflect.KFunction
import kotlin.reflect.jvm.javaMethod

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

val clazz = method.declaringClass
val className = method.declaringClass.name
val name = method.name
return "${clazz.name}.$name"
return "$className.$name"
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
/*
* Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.server.engine.internal

import io.ktor.server.application.*
import java.lang.reflect.*
import kotlin.reflect.*
import kotlin.reflect.full.*
import kotlin.reflect.jvm.*
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Modifier
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.full.functions
import kotlin.reflect.jvm.javaType
import kotlin.reflect.jvm.kotlinFunction

internal fun executeModuleFunction(
classLoader: ClassLoader,
Expand Down Expand Up @@ -40,10 +44,13 @@ internal fun executeModuleFunction(

try {
if (Function1::class.java.isAssignableFrom(clazz)) {
val constructor = clazz.declaredConstructors.single()
if (constructor.parameterCount != 0) {
throw ReloadingException("Module function with captured variables cannot be instantiated '$fqName'")
// Normally lambda has a single constructor, but this could change after R8/ProGuard optimizations
val constructors = clazz.declaredConstructors
if (constructors.isEmpty()) {
throw ReloadingException("Module function cannot be instantiated '$fqName'")
}
val constructor = constructors.find { it.parameterCount == 0 }
?: throw ReloadingException("Module function with captured variables cannot be instantiated '$fqName'")

constructor.isAccessible = true
@Suppress("UNCHECKED_CAST")
Expand Down

0 comments on commit c718419

Please sign in to comment.