diff --git a/module/basic/basic-configuration/src/main/kotlin/taboolib/module/configuration/ConfigFile.kt b/module/basic/basic-configuration/src/main/kotlin/taboolib/module/configuration/ConfigFile.kt index 71ba1d024..5d4252a40 100644 --- a/module/basic/basic-configuration/src/main/kotlin/taboolib/module/configuration/ConfigFile.kt +++ b/module/basic/basic-configuration/src/main/kotlin/taboolib/module/configuration/ConfigFile.kt @@ -10,27 +10,31 @@ import java.io.File import java.io.InputStream import java.io.Reader import java.text.SimpleDateFormat +import java.util.concurrent.atomic.AtomicInteger /** - * TabooLib - * taboolib.module.configuration.ConfigFile + * TabooLib taboolib.module.configuration.ConfigFile * - * @author mac - * @since 2021/11/22 12:49 上午 - */ -/** * 表示一个配置文件,继承自 ConfigSection 并实现 Configuration 接口。 * * @property file 与此配置关联的文件对象,可为 null * @property name 配置文件的名称(不包含扩展名) * * @param root 根配置对象 + * + * @author mac + * @since 2021/11/22 12:49 上午 */ open class ConfigFile(root: Config) : ConfigSection(root), Configuration { override var file: File? = null override var name: String = "" + private val reloadGenerationCounter = AtomicInteger(0) + + override val reloadGeneration: Int + get() = reloadGenerationCounter.get() + // 存储重载回调的列表 private val reloadCallback = ArrayList() @@ -88,6 +92,7 @@ open class ConfigFile(root: Config) : ConfigSection(root), Configuration { warning("File: $file") throw ex } + reloadGenerationCounter.incrementAndGet() reloadCallback.forEach { it.run() } } @@ -105,6 +110,7 @@ open class ConfigFile(root: Config) : ConfigSection(root), Configuration { warning("Source: \n$contents") throw t } + reloadGenerationCounter.incrementAndGet() reloadCallback.forEach { it.run() } } @@ -116,6 +122,7 @@ open class ConfigFile(root: Config) : ConfigSection(root), Configuration { override fun loadFromReader(reader: Reader) { clear() parser().parse(reader, root, ParsingMode.REPLACE) + reloadGenerationCounter.incrementAndGet() reloadCallback.forEach { it.run() } } @@ -127,6 +134,7 @@ open class ConfigFile(root: Config) : ConfigSection(root), Configuration { override fun loadFromInputStream(inputStream: InputStream) { clear() parser().parse(inputStream, root, ParsingMode.REPLACE) + reloadGenerationCounter.incrementAndGet() reloadCallback.forEach { it.run() } } diff --git a/module/basic/basic-configuration/src/main/kotlin/taboolib/module/configuration/Configuration.kt b/module/basic/basic-configuration/src/main/kotlin/taboolib/module/configuration/Configuration.kt index 13486e7c4..8b10692d5 100644 --- a/module/basic/basic-configuration/src/main/kotlin/taboolib/module/configuration/Configuration.kt +++ b/module/basic/basic-configuration/src/main/kotlin/taboolib/module/configuration/Configuration.kt @@ -23,6 +23,24 @@ interface Configuration : ConfigurationSection { */ var file: File? + /** + * 重载代数,用于缓存失效(例如 `ReloadAwareLazy`)。 + * + * 默认使用“文件指纹”或“内容哈希”来提供一个会随配置变化而变化的值: + * - 若存在关联文件,则使用 `lastModified XOR length` + * - 否则使用 `saveToString().hashCode()` + * + * 如需更高性能或更精确的控制,可在实现中覆盖为实例级别的计数器/版本号。 + */ + val reloadGeneration: Int + get() { + val file = file + if (file != null && file.exists()) { + return (file.lastModified() xor file.length()).hashCode() + } + return saveToString().hashCode() + } + /** * 保存为字符串 */ @@ -322,4 +340,4 @@ interface Configuration : ConfigurationSection { } } } -} \ No newline at end of file +} diff --git a/module/basic/basic-configuration/src/main/kotlin/taboolib/module/configuration/util/Lazy.kt b/module/basic/basic-configuration/src/main/kotlin/taboolib/module/configuration/util/Lazy.kt new file mode 100644 index 000000000..2fcfabbd9 --- /dev/null +++ b/module/basic/basic-configuration/src/main/kotlin/taboolib/module/configuration/util/Lazy.kt @@ -0,0 +1,98 @@ +package taboolib.module.configuration.util + +import taboolib.module.configuration.Configuration +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +/** + * 一个“按 Key 失效”的懒加载委托。 + * + * - 首次访问时执行 [initializer] 并缓存结果 + * - 之后每次访问都会计算 [key],当 Key 发生变化时会重新执行 [initializer] + * - 线程安全:使用双重检查 + `synchronized` 保护初始化与更新 + */ +class KeyedLazy(private val key: () -> Any?, private val initializer: () -> T) : ReadOnlyProperty { + + private val lock = Any() + + @Volatile + private var state: State? = null + + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + val currentKey = key() + val current = state + if (current != null && current.key == currentKey) { + return current.value + } + + return synchronized(lock) { + val latestKey = key() + val latest = state + if (latest != null && latest.key == latestKey) { + return@synchronized latest.value + } + + val newValue = initializer() + state = State(latestKey, newValue) + newValue + } + } + + private data class State(val key: Any?, val value: T) +} + +/** + * 一个“随配置重载代数失效”的懒加载委托。 + * + * - 以 [Configuration.reloadGeneration](或其他可变 Key)作为失效条件 + * - 当代数变化时重新执行 [initializer] 并更新缓存 + * - 线程安全:使用双重检查 + `synchronized` 保护初始化与更新 + * + * `config` 的用法: + * - 传入 [Configuration]:使用其 [Configuration.reloadGeneration] + * - 传入 `File`:使用 `lastModified XOR length` 作为指纹 + * - 传入 `() -> Any?`:使用返回值作为指纹(推荐,最灵活) + * - 传入其他对象:使用 `hashCode()` 作为指纹(通常不会随内容变化) + * - 传入 `null`:若 `thisRef` 是 [Configuration],则使用 `thisRef.reloadGeneration`;否则将不会自动失效 + */ +class ReloadAwareLazy(private val config: Any? = null, private val initializer: () -> T) : ReadOnlyProperty { + + private val lock = Any() + + @Volatile + private var state: State? = null + + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + val currentKey = resolveKey(thisRef) + val current = state + if (current != null && current.key == currentKey) { + return current.value + } + + return synchronized(lock) { + val latestKey = resolveKey(thisRef) + val latest = state + if (latest != null && latest.key == latestKey) { + return@synchronized latest.value + } + + val newValue = initializer() + state = State(latestKey, newValue) + newValue + } + } + + private fun resolveKey(thisRef: Any?): Any? { + return when (val c = config) { + is Configuration -> c.reloadGeneration + is java.io.File -> FileFingerprint(c.lastModified(), c.length()) + is Function0<*> -> c.invoke() + null -> (thisRef as? Configuration)?.reloadGeneration ?: 0 + else -> c.hashCode() + } + } + + private data class FileFingerprint(val lastModified: Long, val length: Long) + + private data class State(val key: Any?, val value: T) +} diff --git a/module/basic/basic-configuration/src/test/kotlin/taboolib/module/configuration/util/LazyTest.kt b/module/basic/basic-configuration/src/test/kotlin/taboolib/module/configuration/util/LazyTest.kt new file mode 100644 index 000000000..24fc3967c --- /dev/null +++ b/module/basic/basic-configuration/src/test/kotlin/taboolib/module/configuration/util/LazyTest.kt @@ -0,0 +1,59 @@ +package taboolib.module.configuration.util + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import taboolib.module.configuration.Configuration +import java.io.File + +class LazyTest { + + @Test + fun keyedLazyCachesUntilKeyChanges() { + class Holder { + var key: Int = 0 + var initCount: Int = 0 + val value: Int by KeyedLazy({ key }) { ++initCount } + } + + val holder = Holder() + assertEquals(1, holder.value) + assertEquals(1, holder.value) + + holder.key = 1 + assertEquals(2, holder.value) + assertEquals(2, holder.value) + } + + @Test + fun reloadAwareLazyInvalidatesWhenConfigReloads() { + val config = Configuration.loadFromString("a: 1") + var initCount = 0 + + val value: Int by ReloadAwareLazy(config) { ++initCount } + + assertEquals(1, value) + assertEquals(1, value) + + config.loadFromString("a: 2") + assertEquals(2, value) + assertEquals(2, value) + } + + @Test + fun reloadAwareLazyInvalidatesWhenFileFingerprintChanges(@TempDir tempDir: File) { + val file = File(tempDir, "config.txt").apply { writeText("1") } + var initCount = 0 + + val value: Int by ReloadAwareLazy(file) { ++initCount } + + val firstLength = file.length() + assertEquals(1, value) + assertEquals(1, value) + + file.writeText("11") + assertEquals(firstLength + 1, file.length()) + assertEquals(2, value) + assertEquals(2, value) + } +}