diff --git a/modules/Gem/pom.xml b/modules/Gem/pom.xml
index 6476837..f74c4bc 100644
--- a/modules/Gem/pom.xml
+++ b/modules/Gem/pom.xml
@@ -66,6 +66,10 @@
placeholderapi
https://repo.extendedclip.com/content/repositories/placeholderapi/
+
+ helpch
+ https://repo.helpch.at/releases/
+
@@ -78,7 +82,7 @@
me.clip
placeholderapi
- 2.11.1
+ 2.11.5
provided
diff --git a/modules/GemShop/pom.xml b/modules/GemShop/pom.xml
index d416f07..5e5592e 100644
--- a/modules/GemShop/pom.xml
+++ b/modules/GemShop/pom.xml
@@ -72,10 +72,34 @@
com.mcatk
- gem
+ Gem
jar
1.0.0
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+ org.mockito
+ mockito-inline
+ 5.2.0
+ test
+
+
+ net.bytebuddy
+ byte-buddy
+ 1.14.17
+ test
+
+
+ net.bytebuddy
+ byte-buddy-agent
+ 1.14.17
+ test
+
diff --git a/modules/GemShop/src/main/java/com/mcatk/gemshop/shops/itemshop/ItemShop.java b/modules/GemShop/src/main/java/com/mcatk/gemshop/shops/itemshop/ItemShop.java
index 72ff5f0..803a346 100644
--- a/modules/GemShop/src/main/java/com/mcatk/gemshop/shops/itemshop/ItemShop.java
+++ b/modules/GemShop/src/main/java/com/mcatk/gemshop/shops/itemshop/ItemShop.java
@@ -5,12 +5,28 @@
import com.mcatk.gemshop.Message;
import org.apache.commons.lang.text.StrBuilder;
import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
-import org.bukkit.scheduler.BukkitRunnable;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.util.HashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
public class ItemShop {
+ private final ExecutorService ioExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(r, "GemShop-IO-Thread");
+ t.setDaemon(true);
+ return t;
+ }
+ });
+
private HashMap itemsMap;
public ItemShop() {
@@ -58,14 +74,40 @@ public void addItem(Player player, String shopId, String itemId, String price) {
}
itemsMap.get(shopId).getMap().put(itemId, item);
GemShop.getPlugin().getConfig().set("Items." + shopId + "." + itemId, item);
- GemShop.getPlugin().saveConfig();
+ saveConfigAsync();
player.sendMessage(Message.INFO + "添加成功:" + itemId + " " + price + "宝石");
}
public void delItem(String shopId, String itemId) {
itemsMap.get(shopId).getMap().remove(itemId);
GemShop.getPlugin().getConfig().set("Items." + shopId + "." + itemId, null);
- GemShop.getPlugin().saveConfig();
+ saveConfigAsync();
+ }
+
+ private void saveConfigAsync() {
+ if (!(GemShop.getPlugin().getConfig() instanceof YamlConfiguration)) {
+ GemShop.getPlugin().saveConfig();
+ return;
+ }
+
+ YamlConfiguration yaml = (YamlConfiguration) GemShop.getPlugin().getConfig();
+ final String data = yaml.saveToString();
+ final File file = new File(GemShop.getPlugin().getDataFolder(), "config.yml");
+
+ ioExecutor.submit(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (file.getParentFile() != null && !file.getParentFile().exists()) {
+ file.getParentFile().mkdirs();
+ }
+ Files.write(file.toPath(), data.getBytes(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ GemShop.getPlugin().getLogger().severe("Could not save config.yml asynchronously!");
+ e.printStackTrace();
+ }
+ }
+ });
}
public Items getItems(String shopId) {
diff --git a/modules/GemShop/src/test/java/com/mcatk/gemshop/shops/itemshop/ItemShopBenchmarkTest.java b/modules/GemShop/src/test/java/com/mcatk/gemshop/shops/itemshop/ItemShopBenchmarkTest.java
new file mode 100644
index 0000000..2f04144
--- /dev/null
+++ b/modules/GemShop/src/test/java/com/mcatk/gemshop/shops/itemshop/ItemShopBenchmarkTest.java
@@ -0,0 +1,113 @@
+package com.mcatk.gemshop.shops.itemshop;
+
+import com.mcatk.gemshop.GemShop;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.PlayerInventory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.util.logging.Logger;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.*;
+
+public class ItemShopBenchmarkTest {
+
+ private GemShop mockPlugin;
+ private YamlConfiguration mockConfig;
+ private Player mockPlayer;
+ private PlayerInventory mockInventory;
+
+ @Before
+ public void setUp() throws Exception {
+ mockPlugin = Mockito.mock(GemShop.class);
+ mockConfig = Mockito.mock(YamlConfiguration.class);
+ mockPlayer = Mockito.mock(Player.class);
+ mockInventory = Mockito.mock(PlayerInventory.class);
+ Logger mockLogger = Mockito.mock(Logger.class);
+
+ // Inject mock plugin
+ Field pluginField = GemShop.class.getDeclaredField("plugin");
+ pluginField.setAccessible(true);
+ pluginField.set(null, mockPlugin);
+
+ when(mockPlugin.getConfig()).thenReturn(mockConfig);
+ when(mockPlugin.getLogger()).thenReturn(mockLogger);
+ // Fix NPE for new File(dataFolder, ...)
+ File tempFolder = new File(System.getProperty("java.io.tmpdir"));
+ when(mockPlugin.getDataFolder()).thenReturn(tempFolder);
+
+ when(mockPlayer.getInventory()).thenReturn(mockInventory);
+ when(mockInventory.getItemInMainHand()).thenReturn(Mockito.mock(ItemStack.class));
+
+ when(mockConfig.getConfigurationSection(anyString())).thenReturn(null);
+ when(mockConfig.saveToString()).thenReturn("key: value");
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ // Reset the static field
+ Field pluginField = GemShop.class.getDeclaredField("plugin");
+ pluginField.setAccessible(true);
+ pluginField.set(null, null);
+ }
+
+ @Test
+ public void testAddItemPerformance() {
+ // Setup slow saveConfig (which should NOT be called now)
+ doAnswer(new Answer() {
+ @Override
+ public Void answer(InvocationOnMock invocation) throws Throwable {
+ Thread.sleep(50); // Simulate 50ms Disk I/O
+ return null;
+ }
+ }).when(mockPlugin).saveConfig();
+
+ ItemShop itemShop = new ItemShop();
+
+ long startTime = System.currentTimeMillis();
+ // Price "100" as string
+ itemShop.addItem(mockPlayer, "shop1", "item1", "100");
+ long endTime = System.currentTimeMillis();
+
+ long duration = endTime - startTime;
+ System.out.println("Benchmark Execution time: " + duration + "ms");
+
+ // Assert that saveConfig was NOT called (because we use async save)
+ verify(mockPlugin, never()).saveConfig();
+
+ // Assert that saveToString WAS called
+ verify(mockConfig).saveToString();
+
+ // In optimized code, duration should be near 0ms
+ if (duration >= 50) {
+ throw new RuntimeException("Execution time too slow: " + duration + "ms. Optimization failed?");
+ }
+ }
+
+ @Test
+ public void testDelItemOptimization() {
+ // Setup initial state
+ ItemShop itemShop = new ItemShop();
+ itemShop.addItem(mockPlayer, "shop1", "item1", "100");
+
+ // Reset mocks to clear interactions from addItem
+ Mockito.clearInvocations(mockConfig, mockPlugin);
+
+ itemShop.delItem("shop1", "item1");
+
+ // Assert that saveConfig was NOT called
+ verify(mockPlugin, never()).saveConfig();
+
+ // Assert that saveToString WAS called
+ verify(mockConfig).saveToString();
+ }
+}