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());
+ }
+}