Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fallback solution for Services #2511

Merged
merged 15 commits into from
Mar 11, 2023
40 changes: 40 additions & 0 deletions mirai-core-utils/src/commonMain/kotlin/Services.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,49 @@

package net.mamoe.mirai.utils

import kotlinx.atomicfu.locks.reentrantLock
import kotlinx.atomicfu.locks.withLock
import kotlin.jvm.JvmName
import kotlin.reflect.KClass

public object Services {
private val lock = reentrantLock()
public fun <T : Any> qualifiedNameOrFail(clazz: KClass<out T>): String =
clazz.qualifiedName ?: error("Could not find qualifiedName for $clazz")

private class Implementation(
val implementationClass: String,
val instance: Lazy<Any>
)

private val registered: MutableMap<String, MutableList<Implementation>> = mutableMapOf()

public fun register(baseClass: String, implementationClass: String, implementation: () -> Any) {
lock.withLock {
registered.getOrPut(baseClass, ::mutableListOf)
.add(Implementation(implementationClass, lazy(implementation)))
}
}

public fun firstImplementationOrNull(baseClass: String): Any? {
lock.withLock {
return registered[baseClass]?.firstOrNull()?.instance?.value
}
}

public fun implementations(baseClass: String): List<Lazy<Any>>? {
lock.withLock {
return registered[baseClass]?.map { it.instance }
}
}

public fun print(): String {
lock.withLock {
return registered.entries.joinToString { "${it.key}:${it.value}" }
}
}
}

public expect fun <T : Any> loadServiceOrNull(clazz: KClass<out T>, fallbackImplementation: String? = null): T?
public expect fun <T : Any> loadService(clazz: KClass<out T>, fallbackImplementation: String? = null): T
public expect fun <T : Any> loadServices(clazz: KClass<out T>): Sequence<T>
Expand Down
44 changes: 40 additions & 4 deletions mirai-core-utils/src/jvmBaseMain/kotlin/Services.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,38 @@ import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.full.createInstance

private enum class LoaderType {
JDK,
BOTH,
FALLBACK,
}

private val loaderType = when (systemProp("mirai.service.loader", "both")) {
"jdk" -> LoaderType.JDK
"both" -> LoaderType.BOTH
"fallback" -> LoaderType.FALLBACK
else -> throw IllegalStateException("cannot find a service loader, mirai.service.loader must be both, jdk or fallback (default by both)")
}

private fun <T : Any> loadServiceByJdk(clazz: KClass<out T>): ServiceLoader<out T> = ServiceLoader.load(clazz.java)

@Suppress("UNCHECKED_CAST")
public actual fun <T : Any> loadService(clazz: KClass<out T>, fallbackImplementation: String?): T {
val fallbackService by lazy {
Services.firstImplementationOrNull(Services.qualifiedNameOrFail(clazz)) as T?
}

var suppressed: Throwable? = null
return ServiceLoader.load(clazz.java).firstOrNull()
?: (if (fallbackImplementation == null) null
else runCatching { findCreateInstance<T>(fallbackImplementation) }.onFailure { suppressed = it }.getOrNull())

val services = when (loaderType) {
LoaderType.JDK -> loadServiceByJdk(clazz).firstOrNull()
LoaderType.BOTH -> loadServiceByJdk(clazz).firstOrNull() ?: fallbackService
LoaderType.FALLBACK -> fallbackService
} ?: if (fallbackImplementation != null) {
runCatching { findCreateInstance<T>(fallbackImplementation) }.onFailure { suppressed = it }.getOrNull()
} else null

return services
?: throw NoSuchElementException("Could not find an implementation for service class ${clazz.qualifiedName}").apply {
if (suppressed != null) addSuppressed(suppressed)
}
Expand All @@ -33,6 +60,15 @@ public actual fun <T : Any> loadServiceOrNull(clazz: KClass<out T>, fallbackImpl
else runCatching { findCreateInstance<T>(fallbackImplementation) }.getOrNull()
}

@Suppress("UNCHECKED_CAST")
public actual fun <T : Any> loadServices(clazz: KClass<out T>): Sequence<T> {
return ServiceLoader.load(clazz.java).asSequence()
val fallBackServicesSeq: Sequence<T> by lazy {
Services.implementations(Services.qualifiedNameOrFail(clazz))?.map { it.value as T }.orEmpty().asSequence()
}

return when (loaderType) {
LoaderType.JDK -> loadServiceByJdk(clazz).asSequence()
LoaderType.BOTH -> loadServiceByJdk(clazz).asSequence().plus(fallBackServicesSeq)
LoaderType.FALLBACK -> fallBackServicesSeq
}
}
45 changes: 2 additions & 43 deletions mirai-core-utils/src/nativeMain/kotlin/Service.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,47 +11,9 @@

package net.mamoe.mirai.utils

import kotlinx.atomicfu.locks.reentrantLock
import kotlinx.atomicfu.locks.withLock
import net.mamoe.mirai.utils.Services.qualifiedNameOrFail
import kotlin.reflect.KClass

public object Services {
private val lock = reentrantLock()

private class Implementation(
val implementationClass: String,
val instance: Lazy<Any>
)

private val registered: MutableMap<String, MutableList<Implementation>> = mutableMapOf()

public fun register(baseClass: String, implementationClass: String, implementation: () -> Any) {
lock.withLock {
registered.getOrPut(baseClass, ::mutableListOf)
.add(Implementation(implementationClass, lazy(implementation)))
}
}

public fun firstImplementationOrNull(baseClass: String): Any? {
lock.withLock {
return registered[baseClass]?.firstOrNull()?.instance?.value
}
}

public fun implementations(baseClass: String): List<Lazy<Any>>? {
lock.withLock {
return registered[baseClass]?.map { it.instance }
}

}

public fun print(): String {
lock.withLock {
return registered.entries.joinToString { "${it.key}:${it.value}" }
}
}
}

@Suppress("UNCHECKED_CAST")
public actual fun <T : Any> loadServiceOrNull(
clazz: KClass<out T>,
Expand All @@ -66,7 +28,4 @@ public actual fun <T : Any> loadService(
?: error("Could not load service '${clazz.qualifiedName ?: clazz}'. Current services: ${Services.print()}")

public actual fun <T : Any> loadServices(clazz: KClass<out T>): Sequence<T> =
Services.implementations(qualifiedNameOrFail(clazz))?.asSequence()?.map { it.value }.orEmpty().castUp()

private fun <T : Any> qualifiedNameOrFail(clazz: KClass<out T>) =
clazz.qualifiedName ?: error("Could not find qualifiedName for $clazz")
Services.implementations(qualifiedNameOrFail(clazz))?.asSequence()?.map { it.value }.orEmpty().castUp()