Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>): 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<String>): List<String>? {
return commandBase.suggest(CommandContext(sender, command, name, commandBase, newParser, args))
}
}
)
}

/**
* 注册一个命令
*
Expand All @@ -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<String>): 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<String>): List<String>? {
val commandBase = CommandBase().also(commandBuilder)
return commandBase.suggest(CommandContext(sender, command, name, commandBase, newParser, args))
}
},
// 复用注册阶段构建的命令树
handlers.executor,
handlers.completer,
// 传入原始命令构建器
commandBuilder
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Boolean>() }

internal var commandIncorrectSender: CommandUnknownNotify<*> =
CommandUnknownNotify(ProxyCommandSender::class.java) { sender, _, _, _ ->
Expand Down Expand Up @@ -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()) {
// 获取下级节点
Expand All @@ -84,7 +97,7 @@ class CommandBase : CommandComponent(-1, false) {
} else {
commandExecutor!!.exec(this, context, "")
}
result
currentResult()
} else {
commandIncorrectCommand.exec(context, -1, 1)
false
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<CommandBase>()
val handlers = createCommandHandlers(false) {
builds.incrementAndGet()
execute(ProxyCommandSender::class.java) { _, context, _ ->
commandBases += context.commandCompound
}
dynamic("value") {
suggestionUncheck<ProxyCommandSender> { _, 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<Boolean> {
handlers.executor.execute(TestSender("false"), command, command.name, emptyArray())
}
val trueFuture = executor.submit<Boolean> {
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -25,9 +28,9 @@
@SuppressWarnings("CallToPrintStackTrace")
public class ClassVisitorHandler {

private static final NavigableMap<Byte, VisitorGroup> propertyMap = Collections.synchronizedNavigableMap(new TreeMap<>());
private static final Map<LifeCycle, Set<ReflexClass>> delayedClasses = Collections.synchronizedMap(new HashMap<>());
private static Set<ReflexClass> classes = null;
private static final NavigableMap<Byte, VisitorGroup> propertyMap = new ConcurrentSkipListMap<>();
private static final Map<LifeCycle, Set<ReflexClass>> delayedClasses = new ConcurrentHashMap<>();
private static volatile Set<ReflexClass> classes = null;

/**
* 初始化函数
Expand All @@ -49,43 +52,59 @@ static void init() {
* 获取能够被 ClassVisitor 访问到的所有类
*/
public static Set<ReflexClass> getClasses() {
if (classes == null) {
long time = TabooLib.execution(() -> {
// 获取所有类
// 这里会首次触发 runningClassMapInJar 的初始化
Map<String, ReflexClass> allClasses = ProjectScannerKt.getRunningClassMap();
// 第一阶段:基于类名快速过滤(不触发反序列化)
long phase1Start = System.currentTimeMillis();
List<Map.Entry<String, ReflexClass>> 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<ReflexClass> getOrInitializeClasses(Supplier<Set<ReflexClass>> initializer) {
Set<ReflexClass> current = classes;
if (current == null) {
synchronized (ClassVisitorHandler.class) {
current = classes;
if (current == null) {
Set<ReflexClass> initialized = Objects.requireNonNull(initializer.get(), "Class initializer returned null");
current = Collections.unmodifiableSet(new LinkedHashSet<>(initialized));
classes = current;
}
}
}
return classes;
return current;
}

private static Set<ReflexClass> scanClasses() {
long startTime = System.currentTimeMillis();
// 获取所有类
// 这里会首次触发 runningClassMapInJar 的初始化
Map<String, ReflexClass> allClasses = ProjectScannerKt.getRunningClassMap();
// 第一阶段:基于类名快速过滤(不触发反序列化)
long phase1Start = System.currentTimeMillis();
List<Map.Entry<String, ReflexClass>> 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<ReflexClass> 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;
}

/**
Expand Down Expand Up @@ -263,7 +282,7 @@ public static void injectAll(@NotNull ReflexClass clazz) {
public static void injectAll(@NotNull LifeCycle lifeCycle) {
long startTime = System.currentTimeMillis();
// 处理延迟注入的类
final Set<ReflexClass> delayedForThisCycle = delayedClasses.get(lifeCycle);
final Set<ReflexClass> delayedForThisCycle = delayedClasses.remove(lifeCycle);
if (delayedForThisCycle != null) {
final List<LifeCycle> cyclesUtilNow = Arrays.stream(LifeCycle.values()).filter(cycle -> cycle.ordinal() < lifeCycle.ordinal()).collect(Collectors.toList());
for (final LifeCycle cycle : cyclesUtilNow) {
Expand All @@ -273,7 +292,6 @@ public static void injectAll(@NotNull LifeCycle lifeCycle) {
}
}
}
delayedClasses.remove(lifeCycle);
}
// 处理正常的类注入
Set<ReflexClass> allClasses = getClasses();
Expand Down Expand Up @@ -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;
}
}
Expand Down
Loading
Loading