From a0394d05bc930346a1c68a182d8b94daefc5b8fb Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 16:22:58 +0000 Subject: [PATCH] Optimize MedalPapi with SQLManager caching - Implemented caching in SQLManager using ConcurrentHashMap - Added MedalPlayerListener to clear cache on player quit - Refactored SQLManager for testability (dependency injection) - Added unit tests verifying cache hits, misses, and null handling - Updated pom.xml with JUnit 5 and Mockito dependencies - Updated PlaceholderAPI repository to helpch.at Co-authored-by: acsoto <59144459+acsoto@users.noreply.github.com> --- modules/MedalCabinet/pom.xml | 24 +++- .../com/mcatk/medalcabinet/MedalCabinet.java | 2 + .../listener/MedalPlayerListener.java | 14 +++ .../mcatk/medalcabinet/sql/SQLManager.java | 28 +++++ .../medalcabinet/sql/SQLManagerTest.java | 104 ++++++++++++++++++ 5 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/listener/MedalPlayerListener.java create mode 100644 modules/MedalCabinet/src/test/java/com/mcatk/medalcabinet/sql/SQLManagerTest.java diff --git a/modules/MedalCabinet/pom.xml b/modules/MedalCabinet/pom.xml index 904e558..1bec4d7 100644 --- a/modules/MedalCabinet/pom.xml +++ b/modules/MedalCabinet/pom.xml @@ -66,6 +66,10 @@ placeholderapi https://repo.extendedclip.com/content/repositories/placeholderapi/ + + helpchat + https://repo.helpch.at/releases/ + @@ -78,8 +82,26 @@ me.clip placeholderapi - 2.11.1 + 2.11.5 provided + + org.junit.jupiter + junit-jupiter + 5.9.2 + test + + + org.mockito + mockito-core + 4.11.0 + test + + + org.mockito + mockito-junit-jupiter + 4.11.0 + test + diff --git a/modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/MedalCabinet.java b/modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/MedalCabinet.java index 63e7818..2462e52 100644 --- a/modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/MedalCabinet.java +++ b/modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/MedalCabinet.java @@ -3,6 +3,7 @@ import com.mcatk.medalcabinet.command.MedalAdminCmd; import com.mcatk.medalcabinet.command.MedalShowCmd; import com.mcatk.medalcabinet.command.MedalUsualCmd; +import com.mcatk.medalcabinet.listener.MedalPlayerListener; import com.mcatk.medalcabinet.papi.MedalPapi; import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; @@ -21,6 +22,7 @@ public void onEnable() { saveDefaultConfig(); regCommand(); regDependency(); + Bukkit.getPluginManager().registerEvents(new MedalPlayerListener(), this); getLogger().info("启动成功"); } diff --git a/modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/listener/MedalPlayerListener.java b/modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/listener/MedalPlayerListener.java new file mode 100644 index 0000000..c1d08ac --- /dev/null +++ b/modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/listener/MedalPlayerListener.java @@ -0,0 +1,14 @@ +package com.mcatk.medalcabinet.listener; + +import com.mcatk.medalcabinet.sql.SQLManager; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; + +public class MedalPlayerListener implements Listener { + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent e) { + SQLManager.getInstance().clearCache(e.getPlayer().getName()); + } +} diff --git a/modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/sql/SQLManager.java b/modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/sql/SQLManager.java index a719230..022d8c4 100644 --- a/modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/sql/SQLManager.java +++ b/modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/sql/SQLManager.java @@ -5,20 +5,32 @@ import java.sql.*; import java.util.ArrayList; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; public class SQLManager { private Connection connection; private static SQLManager instance = null; + private final Map mainMedalCache = new ConcurrentHashMap<>(); + private static final Medal EMPTY_MEDAL = new Medal("", "", "", ""); public static SQLManager getInstance() { return instance == null ? instance = new SQLManager() : instance; } + public static void setInstance(SQLManager instance) { + SQLManager.instance = instance; + } + private SQLManager() { connectMySQL(); } + protected SQLManager(Connection connection) { + this.connection = connection; + } + private void connectMySQL() { String ip = MedalCabinet.getPlugin().getConfig().getString("mysql.ip"); String databaseName = MedalCabinet.getPlugin().getConfig().getString("mysql.databasename"); @@ -157,6 +169,12 @@ public boolean setMainMedal(String playerID, String medalID) { ps.setString(2, medalID); ps.setString(3, medalID); ps.executeUpdate(); + + Medal newMain = getMedal(medalID); + if (newMain != null) { + mainMedalCache.put(playerID, newMain); + } + return true; } catch (SQLException e) { e.printStackTrace(); @@ -166,6 +184,11 @@ public boolean setMainMedal(String playerID, String medalID) { } public Medal getMainMedal(String playerID) { + if (mainMedalCache.containsKey(playerID)) { + Medal cached = mainMedalCache.get(playerID); + return cached == EMPTY_MEDAL ? null : cached; + } + Medal medal = null; try (PreparedStatement ps = connection.prepareStatement( "SELECT medal_id FROM `player_main_medal` WHERE player_id = ?" @@ -178,7 +201,12 @@ public Medal getMainMedal(String playerID) { } catch (SQLException e) { e.printStackTrace(); } + mainMedalCache.put(playerID, medal == null ? EMPTY_MEDAL : medal); return medal; } + public void clearCache(String playerID) { + mainMedalCache.remove(playerID); + } + } diff --git a/modules/MedalCabinet/src/test/java/com/mcatk/medalcabinet/sql/SQLManagerTest.java b/modules/MedalCabinet/src/test/java/com/mcatk/medalcabinet/sql/SQLManagerTest.java new file mode 100644 index 0000000..78b80b3 --- /dev/null +++ b/modules/MedalCabinet/src/test/java/com/mcatk/medalcabinet/sql/SQLManagerTest.java @@ -0,0 +1,104 @@ +package com.mcatk.medalcabinet.sql; + +import com.mcatk.medalcabinet.medal.Medal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SQLManagerTest { + + @Mock + private Connection connection; + @Mock + private PreparedStatement preparedStatement; + @Mock + private ResultSet resultSet; + + private SQLManager sqlManager; + + @BeforeEach + void setUp() throws SQLException { + sqlManager = new SQLManager(connection); + SQLManager.setInstance(sqlManager); + + lenient().when(connection.prepareStatement(anyString())).thenReturn(preparedStatement); + lenient().when(preparedStatement.executeQuery()).thenReturn(resultSet); + } + + @Test + void testGetMainMedalCached() throws SQLException { + // Setup mock to return a medal ID "medal1" for player "player1" + + // Simulating: + // First query (get main medal ID) returns "medal1" + // Second query (get medal details) returns medal details + + when(resultSet.next()).thenReturn(true, true); // Sufficient for 1 call (2 queries) + when(resultSet.getString("medal_id")).thenReturn("medal1"); + when(resultSet.getString("medal_name")).thenReturn("Medal One"); + + // Call 1 - Should hit DB + Medal m1 = sqlManager.getMainMedal("player1"); + assertNotNull(m1); + assertEquals("Medal One", m1.getName()); + + // Call 2 - Should NOT hit DB (cached) + Medal m2 = sqlManager.getMainMedal("player1"); + assertNotNull(m2); + assertEquals("Medal One", m2.getName()); + assertEquals(m1, m2); // Should be same object reference if cached + + // Verify that prepareStatement was called exactly 2 times (for the first call only) + verify(connection, times(2)).prepareStatement(anyString()); + + // Clear cache + sqlManager.clearCache("player1"); + + // Setup mock for another call + when(resultSet.next()).thenReturn(true, true); + when(resultSet.getString("medal_id")).thenReturn("medal1"); + when(resultSet.getString("medal_name")).thenReturn("Medal One"); + + // Call 3 - Should hit DB again + Medal m3 = sqlManager.getMainMedal("player1"); + assertNotNull(m3); + + // Verify prepareStatement called 2 more times (total 4) + verify(connection, times(4)).prepareStatement(anyString()); + } + + @Test + void testGetMainMedalNullCaching() throws SQLException { + // Setup mock to return no medal (false on first next()) + when(resultSet.next()).thenReturn(false); + + // Call 1 - Should hit DB (1 query) + Medal m1 = sqlManager.getMainMedal("noplayer"); + assertNull(m1); + + // Call 2 - Should NOT hit DB (cached empty) + Medal m2 = sqlManager.getMainMedal("noplayer"); + assertNull(m2); + + // Verify query count. + // Logic: + // 1. SELECT medal_id ... returns empty (resultSet.next() -> false). + // 2. Returns null. + // 3. Cache put(noplayer, EMPTY_MEDAL). + + // So only 1 query "SELECT medal_id..." executed. + verify(connection, times(1)).prepareStatement(anyString()); + } +}