diff --git a/common-platform-api/src/main/kotlin/taboolib/common/platform/command/CommandRegister.kt b/common-platform-api/src/main/kotlin/taboolib/common/platform/command/CommandRegister.kt index 3c6eb4056..e17f0e843 100644 --- a/common-platform-api/src/main/kotlin/taboolib/common/platform/command/CommandRegister.kt +++ b/common-platform-api/src/main/kotlin/taboolib/common/platform/command/CommandRegister.kt @@ -4,6 +4,26 @@ import taboolib.common.platform.ProxyCommandSender import taboolib.common.platform.command.component.CommandBase import taboolib.common.platform.function.registerCommand +internal data class CommandHandlers(val executor: CommandExecutor, val completer: CommandCompleter) + +internal fun createCommandHandlers(newParser: Boolean, commandBuilder: CommandBase.() -> Unit): CommandHandlers { + val commandBase = CommandBase().also(commandBuilder) + return CommandHandlers( + executor = object : CommandExecutor { + + override fun execute(sender: ProxyCommandSender, command: CommandStructure, name: String, args: Array): Boolean { + return commandBase.execute(CommandContext(sender, command, name, commandBase, newParser, args)) + } + }, + completer = object : CommandCompleter { + + override fun execute(sender: ProxyCommandSender, command: CommandStructure, name: String, args: Array): List? { + return commandBase.suggest(CommandContext(sender, command, name, commandBase, newParser, args)) + } + } + ) +} + /** * 注册一个命令 * @@ -29,25 +49,13 @@ fun command( newParser: Boolean = false, commandBuilder: CommandBase.() -> Unit, ) { + val handlers = createCommandHandlers(newParser, commandBuilder) registerCommand( // 创建命令结构 CommandStructure(name, aliases, description, usage, permission, permissionMessage, permissionDefault, permissionChildren, newParser), - // 创建执行器 - object : CommandExecutor { - - override fun execute(sender: ProxyCommandSender, command: CommandStructure, name: String, args: Array): Boolean { - val commandBase = CommandBase().also(commandBuilder) - return commandBase.execute(CommandContext(sender, command, name, commandBase, newParser, args)) - } - }, - // 创建补全器 - object : CommandCompleter { - - override fun execute(sender: ProxyCommandSender, command: CommandStructure, name: String, args: Array): List? { - val commandBase = CommandBase().also(commandBuilder) - return commandBase.suggest(CommandContext(sender, command, name, commandBase, newParser, args)) - } - }, + // 复用注册阶段构建的命令树 + handlers.executor, + handlers.completer, // 传入原始命令构建器 commandBuilder ) diff --git a/common-platform-api/src/main/kotlin/taboolib/common/platform/command/component/CommandBase.kt b/common-platform-api/src/main/kotlin/taboolib/common/platform/command/component/CommandBase.kt index 82a9abe38..0174d2624 100644 --- a/common-platform-api/src/main/kotlin/taboolib/common/platform/command/component/CommandBase.kt +++ b/common-platform-api/src/main/kotlin/taboolib/common/platform/command/component/CommandBase.kt @@ -6,11 +6,12 @@ import taboolib.common.platform.command.CommandContext import taboolib.common.platform.service.PlatformCommand import taboolib.common.util.subList import taboolib.common.util.t +import java.util.ArrayDeque @Suppress("DuplicatedCode") class CommandBase : CommandComponent(-1, false) { - internal var result = true + private val resultStack = ThreadLocal.withInitial { ArrayDeque() } internal var commandIncorrectSender: CommandUnknownNotify<*> = CommandUnknownNotify(ProxyCommandSender::class.java) { sender, _, _, _ -> @@ -65,7 +66,19 @@ class CommandBase : CommandComponent(-1, false) { } fun execute(context: CommandContext<*>): Boolean { - result = true + val results = resultStack.get() + results.addLast(true) + return try { + executeInternal(context) + } finally { + results.removeLast() + if (results.isEmpty()) { + resultStack.remove() + } + } + } + + private fun executeInternal(context: CommandContext<*>): Boolean { // 空参数是一种特殊的状态,指的是玩家输入根命令且不附带任何参数,例如 [/test] 而不是 [/test ] if (context.realArgs.isEmpty()) { // 获取下级节点 @@ -84,7 +97,7 @@ class CommandBase : CommandComponent(-1, false) { } else { commandExecutor!!.exec(this, context, "") } - result + currentResult() } else { commandIncorrectCommand.exec(context, -1, 1) false @@ -117,7 +130,7 @@ class CommandBase : CommandComponent(-1, false) { } else { find.commandExecutor!!.exec(this, context, context.self()) } - result + currentResult() } else { commandIncorrectCommand.exec(context, cur + 1, 1) false @@ -174,7 +187,17 @@ class CommandBase : CommandComponent(-1, false) { this.commandIncorrectCommand = CommandUnknownNotify(ProxyCommandSender::class.java, function) } + private fun currentResult(): Boolean { + return resultStack.get().peekLast() ?: true + } + fun setResult(value: Boolean) { - result = value + val results = resultStack.get() + if (results.isEmpty()) { + resultStack.remove() + return + } + results.removeLast() + results.addLast(value) } -} \ No newline at end of file +} diff --git a/common-platform-api/src/test/kotlin/taboolib/common/platform/command/CommandRegistrationConcurrencyTest.kt b/common-platform-api/src/test/kotlin/taboolib/common/platform/command/CommandRegistrationConcurrencyTest.kt new file mode 100644 index 000000000..33937ec15 --- /dev/null +++ b/common-platform-api/src/test/kotlin/taboolib/common/platform/command/CommandRegistrationConcurrencyTest.kt @@ -0,0 +1,100 @@ +package taboolib.common.platform.command + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertSame +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import taboolib.common.platform.ProxyCommandSender +import taboolib.common.platform.command.component.CommandBase +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +class CommandRegistrationConcurrencyTest { + + @Test + fun `command tree is built once and reused by executions`() { + val builds = AtomicInteger() + val commandBases = CopyOnWriteArrayList() + val handlers = createCommandHandlers(false) { + builds.incrementAndGet() + execute(ProxyCommandSender::class.java) { _, context, _ -> + commandBases += context.commandCompound + } + dynamic("value") { + suggestionUncheck { _, context -> + commandBases += context.commandCompound + listOf("value") + } + } + } + val command = command() + val sender = TestSender("sender") + + assertTrue(handlers.executor.execute(sender, command, command.name, emptyArray())) + assertEquals(listOf("value"), handlers.completer.execute(sender, command, command.name, arrayOf(""))) + + assertEquals(1, builds.get()) + assertEquals(2, commandBases.size) + assertSame(commandBases.first(), commandBases.last()) + } + + @Test + fun `concurrent executions keep independent result state`() { + val falseResultSet = CountDownLatch(1) + val trueResultSet = CountDownLatch(1) + val handlers = createCommandHandlers(false) { + execute(ProxyCommandSender::class.java) { sender, context, _ -> + if (sender.name == "false") { + context.commandCompound.setResult(false) + falseResultSet.countDown() + assertTrue(trueResultSet.await(5, TimeUnit.SECONDS)) + } else { + assertTrue(falseResultSet.await(5, TimeUnit.SECONDS)) + context.commandCompound.setResult(true) + trueResultSet.countDown() + } + } + } + val command = command() + val executor = Executors.newFixedThreadPool(2) + try { + val falseFuture = executor.submit { + handlers.executor.execute(TestSender("false"), command, command.name, emptyArray()) + } + val trueFuture = executor.submit { + handlers.executor.execute(TestSender("true"), command, command.name, emptyArray()) + } + + assertFalse(falseFuture.get(10, TimeUnit.SECONDS)) + assertTrue(trueFuture.get(10, TimeUnit.SECONDS)) + } finally { + trueResultSet.countDown() + executor.shutdownNow() + executor.awaitTermination(5, TimeUnit.SECONDS) + } + } + + private fun command(): CommandStructure { + return CommandStructure("test", emptyList(), "", "", "", "", PermissionDefault.OP, emptyMap(), false) + } + + private class TestSender(override val name: String) : ProxyCommandSender { + + override val origin: Any + get() = this + + override var isOp = false + + override fun isOnline() = true + + override fun sendMessage(message: String) = Unit + + override fun performCommand(command: String) = true + + override fun hasPermission(permission: String) = true + } +} diff --git a/common-util/src/main/java/taboolib/common/inject/ClassVisitorHandler.java b/common-util/src/main/java/taboolib/common/inject/ClassVisitorHandler.java index 6133fc3a8..d7e3684b6 100644 --- a/common-util/src/main/java/taboolib/common/inject/ClassVisitorHandler.java +++ b/common-util/src/main/java/taboolib/common/inject/ClassVisitorHandler.java @@ -13,6 +13,9 @@ import taboolib.common.platform.DelayTo; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.function.Supplier; import java.util.stream.Collectors; /** @@ -25,9 +28,9 @@ @SuppressWarnings("CallToPrintStackTrace") public class ClassVisitorHandler { - private static final NavigableMap propertyMap = Collections.synchronizedNavigableMap(new TreeMap<>()); - private static final Map> delayedClasses = Collections.synchronizedMap(new HashMap<>()); - private static Set classes = null; + private static final NavigableMap propertyMap = new ConcurrentSkipListMap<>(); + private static final Map> delayedClasses = new ConcurrentHashMap<>(); + private static volatile Set classes = null; /** * 初始化函数 @@ -49,43 +52,59 @@ static void init() { * 获取能够被 ClassVisitor 访问到的所有类 */ public static Set getClasses() { - if (classes == null) { - long time = TabooLib.execution(() -> { - // 获取所有类 - // 这里会首次触发 runningClassMapInJar 的初始化 - Map allClasses = ProjectScannerKt.getRunningClassMap(); - // 第一阶段:基于类名快速过滤(不触发反序列化) - long phase1Start = System.currentTimeMillis(); - List> candidates = allClasses.entrySet().parallelStream() - .filter(entry -> { - String key = entry.getKey(); - // 排除非本项目 && 排除第三方库 && 排除匿名内部类 - return isProjectClass(key) && !isLibraryClass(key) && !isAnonymousInnerClass(key); - }) - .collect(Collectors.toList()); - long phase1Time = System.currentTimeMillis() - phase1Start; - PrimitiveIO.debug("ClassVisitor 第一阶段过滤: {0} -> {1} 个候选类,用时 {2} 毫秒。", allClasses.size(), candidates.size(), phase1Time); - // 第二阶段:并行检查注解和平台条件(会触发反序列化,但只针对候选类) - long phase2Start = System.currentTimeMillis(); - classes = candidates.parallelStream() - .filter(entry -> { - String key = entry.getKey(); - ReflexClass value = entry.getValue(); - // 排除属于 TabooLib 但没有 Inject 注解的类 - if (isTabooLibClass(key) && !value.getStructure().isAnnotationPresent(Inject.class)) { - return false; - } - // 检测有效平台 & 条件注解 - return checkPlatform(value) && checkRequires(value); - }) - .map(Map.Entry::getValue) - .collect(Collectors.toCollection(LinkedHashSet::new)); - long phase2Time = System.currentTimeMillis() - phase2Start; - PrimitiveIO.debug("ClassVisitor 第二阶段过滤: {0} -> {1} 个有效类,用时 {2} 毫秒。", candidates.size(), classes.size(), phase2Time); - }); - PrimitiveIO.debug("ClassVisitor 总用时 {0} 毫秒。", time); + return getOrInitializeClasses(ClassVisitorHandler::scanClasses); + } + + static Set getOrInitializeClasses(Supplier> initializer) { + Set current = classes; + if (current == null) { + synchronized (ClassVisitorHandler.class) { + current = classes; + if (current == null) { + Set initialized = Objects.requireNonNull(initializer.get(), "Class initializer returned null"); + current = Collections.unmodifiableSet(new LinkedHashSet<>(initialized)); + classes = current; + } + } } - return classes; + return current; + } + + private static Set scanClasses() { + long startTime = System.currentTimeMillis(); + // 获取所有类 + // 这里会首次触发 runningClassMapInJar 的初始化 + Map allClasses = ProjectScannerKt.getRunningClassMap(); + // 第一阶段:基于类名快速过滤(不触发反序列化) + long phase1Start = System.currentTimeMillis(); + List> candidates = allClasses.entrySet().parallelStream() + .filter(entry -> { + String key = entry.getKey(); + // 排除非本项目 && 排除第三方库 && 排除匿名内部类 + return isProjectClass(key) && !isLibraryClass(key) && !isAnonymousInnerClass(key); + }) + .collect(Collectors.toList()); + long phase1Time = System.currentTimeMillis() - phase1Start; + PrimitiveIO.debug("ClassVisitor 第一阶段过滤: {0} -> {1} 个候选类,用时 {2} 毫秒。", allClasses.size(), candidates.size(), phase1Time); + // 第二阶段:并行检查注解和平台条件(会触发反序列化,但只针对候选类) + long phase2Start = System.currentTimeMillis(); + Set filteredClasses = candidates.parallelStream() + .filter(entry -> { + String key = entry.getKey(); + ReflexClass value = entry.getValue(); + // 排除属于 TabooLib 但没有 Inject 注解的类 + if (isTabooLibClass(key) && !value.getStructure().isAnnotationPresent(Inject.class)) { + return false; + } + // 检测有效平台 & 条件注解 + return checkPlatform(value) && checkRequires(value); + }) + .map(Map.Entry::getValue) + .collect(Collectors.toCollection(LinkedHashSet::new)); + long phase2Time = System.currentTimeMillis() - phase2Start; + PrimitiveIO.debug("ClassVisitor 第二阶段过滤: {0} -> {1} 个有效类,用时 {2} 毫秒。", candidates.size(), filteredClasses.size(), phase2Time); + PrimitiveIO.debug("ClassVisitor 总用时 {0} 毫秒。", System.currentTimeMillis() - startTime); + return filteredClasses; } /** @@ -263,7 +282,7 @@ public static void injectAll(@NotNull ReflexClass clazz) { public static void injectAll(@NotNull LifeCycle lifeCycle) { long startTime = System.currentTimeMillis(); // 处理延迟注入的类 - final Set delayedForThisCycle = delayedClasses.get(lifeCycle); + final Set delayedForThisCycle = delayedClasses.remove(lifeCycle); if (delayedForThisCycle != null) { final List cyclesUtilNow = Arrays.stream(LifeCycle.values()).filter(cycle -> cycle.ordinal() < lifeCycle.ordinal()).collect(Collectors.toList()); for (final LifeCycle cycle : cyclesUtilNow) { @@ -273,7 +292,6 @@ public static void injectAll(@NotNull LifeCycle lifeCycle) { } } } - delayedClasses.remove(lifeCycle); } // 处理正常的类注入 Set allClasses = getClasses(); @@ -348,7 +366,7 @@ public static void inject(@NotNull ReflexClass clazz, @NotNull VisitorGroup grou if (lifeCycle != null && clazz.getStructure().isAnnotationPresent(DelayTo.class) && !isDelayTo) { final LifeCycle delayTo = clazz.getStructure().getAnnotation(DelayTo.class).getEnum("value", LifeCycle.CONST); if (delayTo.ordinal() > lifeCycle.ordinal()) { - delayedClasses.computeIfAbsent(delayTo, k -> Collections.synchronizedSet(new HashSet<>())).add(clazz); + delayedClasses.computeIfAbsent(delayTo, k -> ConcurrentHashMap.newKeySet()).add(clazz); return; } } diff --git a/common-util/src/main/kotlin/taboolib/common/event/InternalEventBus.kt b/common-util/src/main/kotlin/taboolib/common/event/InternalEventBus.kt index 12df1f83e..abdbd7419 100644 --- a/common-util/src/main/kotlin/taboolib/common/event/InternalEventBus.kt +++ b/common-util/src/main/kotlin/taboolib/common/event/InternalEventBus.kt @@ -46,10 +46,10 @@ interface InternalEventBus { var impl = object : InternalEventBus { /** 已注册的监听器 */ - val registeredListeners = ConcurrentHashMap, MutableMap>>() + val registeredListeners = ConcurrentHashMap, ConcurrentSkipListMap>>() override fun isListening(cls: Class<*>): Boolean { - return registeredListeners.containsKey(cls) && registeredListeners[cls]!!.any { it.value.isNotEmpty() } + return registeredListeners[cls]?.values?.any { it.isNotEmpty() } == true } override fun call(event: T) { @@ -66,7 +66,9 @@ interface InternalEventBus { @Suppress("UNCHECKED_CAST") override fun listen(cls: Class, priority: Int, ignoreCancelled: Boolean, listener: (event: T) -> Unit): InternalListener { val registeredListener = RegisteredListener(cls, priority, ignoreCancelled, listener as (Any) -> Unit) - registeredListeners.getOrPut(cls) { ConcurrentSkipListMap() }.getOrPut(priority) { CopyOnWriteArrayList() }.add(registeredListener) + registeredListeners.computeIfAbsent(cls) { ConcurrentSkipListMap() } + .computeIfAbsent(priority) { CopyOnWriteArrayList() } + .add(registeredListener) return registeredListener } diff --git a/common-util/src/test/kotlin/taboolib/common/event/InternalEventBusConcurrencyTest.kt b/common-util/src/test/kotlin/taboolib/common/event/InternalEventBusConcurrencyTest.kt new file mode 100644 index 000000000..bfddbd344 --- /dev/null +++ b/common-util/src/test/kotlin/taboolib/common/event/InternalEventBusConcurrencyTest.kt @@ -0,0 +1,47 @@ +package taboolib.common.event + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.CyclicBarrier +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +class InternalEventBusConcurrencyTest { + + private class TestEvent : InternalEvent() + + @Test + fun `concurrent listeners at the same priority are not lost`() { + val threadCount = 24 + val executor = Executors.newFixedThreadPool(threadCount) + val registered = CopyOnWriteArrayList() + try { + repeat(50) { round -> + val barrier = CyclicBarrier(threadCount) + val calls = AtomicInteger() + val futures = (0 until threadCount).map { + CompletableFuture.supplyAsync({ + barrier.await(5, TimeUnit.SECONDS) + InternalEventBus.listen(TestEvent::class.java, Int.MIN_VALUE + round, false) { + calls.incrementAndGet() + } + }, executor) + } + futures.forEach { registered += it.get(10, TimeUnit.SECONDS) } + + InternalEventBus.call(TestEvent()) + + assertEquals(threadCount, calls.get(), "round $round lost registered listeners") + registered.forEach(InternalListener::cancel) + registered.clear() + } + } finally { + registered.forEach(InternalListener::cancel) + executor.shutdownNow() + executor.awaitTermination(5, TimeUnit.SECONDS) + } + } +} diff --git a/common-util/src/test/kotlin/taboolib/common/inject/ClassVisitorHandlerConcurrencyTest.kt b/common-util/src/test/kotlin/taboolib/common/inject/ClassVisitorHandlerConcurrencyTest.kt new file mode 100644 index 000000000..3cbe56dbc --- /dev/null +++ b/common-util/src/test/kotlin/taboolib/common/inject/ClassVisitorHandlerConcurrencyTest.kt @@ -0,0 +1,57 @@ +package taboolib.common.inject + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertSame +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.tabooproject.reflex.ReflexClass +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +class ClassVisitorHandlerConcurrencyTest { + + @Test + fun `class set is initialized once and safely published`() { + val classesField = ClassVisitorHandler::class.java.getDeclaredField("classes").also { it.isAccessible = true } + val previous = classesField.get(null) + classesField.set(null, null) + + val threadCount = 16 + val ready = CountDownLatch(threadCount) + val start = CountDownLatch(1) + val initializerStarted = CountDownLatch(1) + val releaseInitializer = CountDownLatch(1) + val initializerCalls = AtomicInteger() + val executor = Executors.newFixedThreadPool(threadCount) + try { + val futures = (0 until threadCount).map { + executor.submit> { + ready.countDown() + assertTrue(start.await(5, TimeUnit.SECONDS)) + ClassVisitorHandler.getOrInitializeClasses { + initializerCalls.incrementAndGet() + initializerStarted.countDown() + assertTrue(releaseInitializer.await(5, TimeUnit.SECONDS)) + emptySet() + } + } + } + + assertTrue(ready.await(5, TimeUnit.SECONDS)) + start.countDown() + assertTrue(initializerStarted.await(5, TimeUnit.SECONDS)) + releaseInitializer.countDown() + + val results = futures.map { it.get(10, TimeUnit.SECONDS) } + assertEquals(1, initializerCalls.get()) + results.drop(1).forEach { assertSame(results.first(), it) } + } finally { + releaseInitializer.countDown() + executor.shutdownNow() + executor.awaitTermination(5, TimeUnit.SECONDS) + classesField.set(null, previous) + } + } +}