From f3e42f5dbf7bac1366a1e18c8688a9ec21cdd8b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 11:32:46 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=BF=9E=E6=8E=A5=E6=B1=A0=E6=87=92?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD+=E6=9C=89=E6=95=88=E6=80=A7=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C,=E9=A9=B1=E5=8A=A8=E8=B5=84=E6=BA=90=E6=B8=85?= =?UTF-8?q?=E7=90=86=E4=B8=8E=E5=8F=8B=E5=A5=BD=E9=94=99=E8=AF=AF=E6=8F=90?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 针对 2.1.0 引入的连接池/驱动加载逻辑做健壮性修复: - QueryTool: 连接池由「一次性预建 10 个连接」改为懒加载,按需创建(上限 10)。 取连接时用 Connection.isValid() 校验,失效连接丢弃并按需重建,避免长时间空闲后 使用到被数据库侧断开的 stale 连接。单个连接创建失败只抛 SQLException 且不污染 连接池,不再首次查询即整体 RuntimeException;归还时校验是否已关闭。 - DataSourceDriverUtil: 跟踪注册的驱动 shim 与 URLClassLoader,新增 deregisterAllDrivers() 在 dispose 时反注册并关闭类加载器,避免长期驻留; 驱动未下载(ClassNotFound/NoClassDefFound)时给出可操作的友好提示,引导用户 先在 DataGrip 数据源里下载该驱动。 - DataPivotInitializer: 项目 dispose 时一并清理连接池与已注册驱动 (此前 closeAllConnections 从未被调用,连接会泄漏)。 - DataGripUtil: collectNamingNames 递归增加 node/group/child/name 的 null 防御。 - i18n: 新增驱动未下载、驱动注册失败的中英文提示文案。 https://claude.ai/code/session_01HYez8dwWXxDUsygNsoKWT9 --- .../plugin/config/DataPivotInitializer.java | 4 + .../data/pivot/plugin/tool/DataGripUtil.java | 11 +- .../plugin/tool/DataSourceDriverUtil.java | 66 ++++++-- .../com/data/pivot/plugin/tool/QueryTool.java | 154 +++++++++++++++--- .../messages/DataPivotBundle.properties | 2 + .../messages/DataPivotBundle_en_US.properties | 2 + .../messages/DataPivotBundle_zh_CN.properties | 2 + 7 files changed, 198 insertions(+), 43 deletions(-) diff --git a/src/main/java/com/data/pivot/plugin/config/DataPivotInitializer.java b/src/main/java/com/data/pivot/plugin/config/DataPivotInitializer.java index c76aefd..c009039 100644 --- a/src/main/java/com/data/pivot/plugin/config/DataPivotInitializer.java +++ b/src/main/java/com/data/pivot/plugin/config/DataPivotInitializer.java @@ -2,7 +2,9 @@ import com.data.pivot.plugin.entity.custom.DataPivotStrategyInfo; +import com.data.pivot.plugin.tool.DataSourceDriverUtil; import com.data.pivot.plugin.tool.DatabaseUtil; +import com.data.pivot.plugin.tool.QueryTool; import com.data.pivot.plugin.context.DataPivotApplication; import com.intellij.openapi.project.Project; import com.intellij.openapi.startup.StartupActivity; @@ -16,6 +18,8 @@ public void runActivity(Project project) { initDataPivotDatabaseInfo(project); initDataPivotSettingInfo(project); Disposer.register(project, DatabaseUtil::closeConnections); + Disposer.register(project, QueryTool::closeAllConnections); + Disposer.register(project, DataSourceDriverUtil::deregisterAllDrivers); } static void initDefaultStrategy(DataPivotApplication application) { diff --git a/src/main/java/com/data/pivot/plugin/tool/DataGripUtil.java b/src/main/java/com/data/pivot/plugin/tool/DataGripUtil.java index b31be6a..672f4c8 100644 --- a/src/main/java/com/data/pivot/plugin/tool/DataGripUtil.java +++ b/src/main/java/com/data/pivot/plugin/tool/DataGripUtil.java @@ -80,9 +80,12 @@ private static List getIntrospectionObjectNames(LocalDataSource dataSour } private static void collectNamingNames(TreePatternNode node, List names) { + if (node == null) { + return; + } if (node.naming != null && node.naming.names != null) { for (ObjectName name : node.naming.names) { - if (name.name != null && !name.name.isBlank() && !names.contains(name.name)) { + if (name != null && name.name != null && !name.name.isBlank() && !names.contains(name.name)) { names.add(name.name); } } @@ -91,11 +94,13 @@ private static void collectNamingNames(TreePatternNode node, List names) return; } for (TreePatternNode.Group group : node.groups) { - if (group.children == null) { + if (group == null || group.children == null) { continue; } for (TreePatternNode child : group.children) { - collectNamingNames(child, names); + if (child != null) { + collectNamingNames(child, names); + } } } } diff --git a/src/main/java/com/data/pivot/plugin/tool/DataSourceDriverUtil.java b/src/main/java/com/data/pivot/plugin/tool/DataSourceDriverUtil.java index 545f3b8..1cb689c 100644 --- a/src/main/java/com/data/pivot/plugin/tool/DataSourceDriverUtil.java +++ b/src/main/java/com/data/pivot/plugin/tool/DataSourceDriverUtil.java @@ -1,5 +1,6 @@ package com.data.pivot.plugin.tool; +import com.data.pivot.plugin.i18n.DataPivotBundle; import com.intellij.database.dataSource.DatabaseDriver; import com.intellij.database.dataSource.LocalDataSource; import com.intellij.database.dataSource.artifacts.DatabaseArtifactContext; @@ -7,6 +8,7 @@ import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.util.ui.classpath.SimpleClasspathElement; +import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Path; @@ -26,7 +28,7 @@ import java.util.stream.Collectors; public final class DataSourceDriverUtil { - private static final ConcurrentMap REGISTERED_DRIVERS = new ConcurrentHashMap<>(); + private static final ConcurrentMap REGISTERED_DRIVERS = new ConcurrentHashMap<>(); private DataSourceDriverUtil() { } @@ -64,32 +66,70 @@ public static void ensureDriverRegistered(String dataSourceId, String driverClas List roots = classRootUrls == null ? Collections.emptyList() : classRootUrls; String key = driverClassName + "@" + String.join("|", roots); - REGISTERED_DRIVERS.computeIfAbsent(key, ignored -> registerDriver(dataSourceId, driverClassName, roots)); + REGISTERED_DRIVERS.computeIfAbsent(key, ignored -> registerDriver(driverClassName, roots)); } - private static Driver registerDriver(String dataSourceId, String driverClassName, List classRootUrls) { + private static RegisteredDriver registerDriver(String driverClassName, List classRootUrls) { + URLClassLoader classLoader = null; try { - Driver driver = createDriver(driverClassName, classRootUrls); + Driver driver; + if (classRootUrls.isEmpty()) { + driver = (Driver) Class.forName(driverClassName).getDeclaredConstructor().newInstance(); + } else { + classLoader = createClassLoader(classRootUrls); + driver = (Driver) Class.forName(driverClassName, true, classLoader).getDeclaredConstructor().newInstance(); + } Driver shim = new DriverShim(driver); DriverManager.registerDriver(shim); - return shim; + return new RegisteredDriver(shim, classLoader); + } catch (ClassNotFoundException | NoClassDefFoundError e) { + // 驱动 jar 尚未下载到本地(或 jar 内不含该驱动类),引导用户先在 DataGrip 数据源里下载该驱动。 + closeQuietly(classLoader); + throw new IllegalStateException( + DataPivotBundle.message("data.pivot.driver.not.downloaded", driverClassName), e); } catch (Exception e) { - throw new IllegalStateException("Failed to register database driver for " + dataSourceId + ": " + driverClassName, e); + closeQuietly(classLoader); + throw new IllegalStateException( + DataPivotBundle.message("data.pivot.driver.register.fail", driverClassName, String.valueOf(e.getMessage())), e); } } - private static Driver createDriver(String driverClassName, List classRootUrls) throws Exception { - if (classRootUrls.isEmpty()) { - return (Driver) Class.forName(driverClassName).getDeclaredConstructor().newInstance(); - } - + private static URLClassLoader createClassLoader(List classRootUrls) throws Exception { List urls = new ArrayList<>(); for (String rootUrl : classRootUrls) { urls.add(toUrl(rootUrl)); } + return new URLClassLoader(urls.toArray(new URL[0]), DataSourceDriverUtil.class.getClassLoader()); + } + + /** + * 释放本工具注册过的驱动 shim 及其 {@link URLClassLoader}。应在项目/插件 dispose 时调用, + * 避免 DriverManager 中的 shim 与类加载器长期驻留;下次查询会按需重新注册。 + */ + public static void deregisterAllDrivers() { + for (RegisteredDriver registered : REGISTERED_DRIVERS.values()) { + try { + DriverManager.deregisterDriver(registered.shim()); + } catch (SQLException ignored) { + // best-effort cleanup on dispose + } + closeQuietly(registered.classLoader()); + } + REGISTERED_DRIVERS.clear(); + } + + private static void closeQuietly(URLClassLoader classLoader) { + if (classLoader == null) { + return; + } + try { + classLoader.close(); + } catch (IOException ignored) { + // best-effort cleanup + } + } - URLClassLoader classLoader = new URLClassLoader(urls.toArray(new URL[0]), DataSourceDriverUtil.class.getClassLoader()); - return (Driver) Class.forName(driverClassName, true, classLoader).getDeclaredConstructor().newInstance(); + private record RegisteredDriver(Driver shim, URLClassLoader classLoader) { } private static URL toUrl(String rootUrl) throws Exception { diff --git a/src/main/java/com/data/pivot/plugin/tool/QueryTool.java b/src/main/java/com/data/pivot/plugin/tool/QueryTool.java index 0e5863e..023c0d1 100644 --- a/src/main/java/com/data/pivot/plugin/tool/QueryTool.java +++ b/src/main/java/com/data/pivot/plugin/tool/QueryTool.java @@ -22,6 +22,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; /** * 查询工具类,复用 IntelliJ IDEA Database Tools 中已配置的数据源驱动。 @@ -31,7 +32,8 @@ public class QueryTool { private static final int LIMIT = 20; private static final int QUERY_TIMEOUT = 5; private static final int MAX_CONNECTIONS = 10; - private static final ConcurrentMap> connectionPools = new ConcurrentHashMap<>(); + private static final int VALIDATION_TIMEOUT = 2; + private static final ConcurrentMap connectionPools = new ConcurrentHashMap<>(); private static Connection getConnection(DatabaseQueryConfig config) throws InterruptedException, SQLException { if (DBType.MONGO.equals(config.getDbType())) { @@ -45,22 +47,12 @@ private static Connection getConnection(DatabaseQueryConfig config) throws Inter ); String poolKey = config.getDataSourceId() + "@" + config.getUrl(); - BlockingQueue pool = getOrCreateConnectionPool(poolKey, config); - return pool.poll(QUERY_TIMEOUT, TimeUnit.SECONDS); + ConnectionPool pool = getOrCreateConnectionPool(poolKey, config); + return pool.acquire(); } - private static BlockingQueue getOrCreateConnectionPool(String poolKey, DatabaseQueryConfig config) { - return connectionPools.computeIfAbsent(poolKey, id -> { - BlockingQueue pool = new LinkedBlockingQueue<>(MAX_CONNECTIONS); - for (int i = 0; i < MAX_CONNECTIONS; i++) { - try { - pool.offer(createConnection(config)); - } catch (SQLException e) { - throw new RuntimeException("Failed to create a new connection", e); - } - } - return pool; - }); + private static ConnectionPool getOrCreateConnectionPool(String poolKey, DatabaseQueryConfig config) { + return connectionPools.computeIfAbsent(poolKey, id -> new ConnectionPool(config)); } private static Connection createConnection(DatabaseQueryConfig config) throws SQLException { @@ -97,10 +89,15 @@ static String addUrlParameters(String url, String parameters) { } private static void releaseConnection(DatabaseQueryConfig config, Connection connection) { + if (connection == null) { + return; + } String poolKey = config.getDataSourceId() + "@" + config.getUrl(); - BlockingQueue pool = connectionPools.get(poolKey); + ConnectionPool pool = connectionPools.get(poolKey); if (pool != null) { - pool.offer(connection); + pool.release(connection); + } else { + ConnectionPool.closeQuietly(connection); } } @@ -197,18 +194,121 @@ private static List> executeQuery(Connection connection, Str } public static void closeAllConnections() { - connectionPools.values().forEach(pool -> { - while (!pool.isEmpty()) { - try { - Connection connection = pool.poll(); - if (connection != null) { - connection.close(); + connectionPools.values().forEach(ConnectionPool::closeAll); + connectionPools.clear(); + } + + /** + * 懒加载连接池:连接按需创建(上限 {@link #MAX_CONNECTIONS}),取用时通过 {@link Connection#isValid(int)} + * 校验有效性,失效连接会被丢弃并按需重建。相比一次性预建连接,既避免首次查询因单个连接创建失败而整体抛错, + * 也避免长时间空闲后使用到已被数据库侧断开的 stale 连接。 + */ + private static final class ConnectionPool { + private final DatabaseQueryConfig config; + private final BlockingQueue idle = new LinkedBlockingQueue<>(MAX_CONNECTIONS); + private final AtomicInteger total = new AtomicInteger(0); + + private ConnectionPool(DatabaseQueryConfig config) { + this.config = config; + } + + /** + * 取出一个可用连接;池内无空闲且未达上限时新建,达到上限则等待归还,超时返回 {@code null}。 + */ + private Connection acquire() throws SQLException, InterruptedException { + long deadlineNanos = System.nanoTime() + TimeUnit.SECONDS.toNanos(QUERY_TIMEOUT); + while (true) { + Connection pooled = idle.poll(); + if (pooled != null) { + if (isUsable(pooled)) { + return pooled; + } + discard(pooled); + continue; + } + if (tryReserveSlot()) { + try { + return createConnection(config); + } catch (SQLException e) { + total.decrementAndGet(); + throw e; } - } catch (SQLException ignored) { - // Ignore close failures while disposing pooled connections. } + long remainingNanos = deadlineNanos - System.nanoTime(); + if (remainingNanos <= 0) { + return null; + } + pooled = idle.poll(remainingNanos, TimeUnit.NANOSECONDS); + if (pooled == null) { + return null; + } + if (isUsable(pooled)) { + return pooled; + } + discard(pooled); } - }); - connectionPools.clear(); + } + + /** + * 归还连接:已关闭或池已满则直接释放,否则放回空闲队列。 + */ + private void release(Connection connection) { + if (connection == null) { + return; + } + if (isClosedQuietly(connection) || !idle.offer(connection)) { + discard(connection); + } + } + + private boolean tryReserveSlot() { + int current; + do { + current = total.get(); + if (current >= MAX_CONNECTIONS) { + return false; + } + } while (!total.compareAndSet(current, current + 1)); + return true; + } + + private void discard(Connection connection) { + closeQuietly(connection); + total.decrementAndGet(); + } + + private void closeAll() { + Connection connection; + while ((connection = idle.poll()) != null) { + closeQuietly(connection); + } + total.set(0); + } + + private static boolean isUsable(Connection connection) { + try { + return connection != null && !connection.isClosed() && connection.isValid(VALIDATION_TIMEOUT); + } catch (SQLException e) { + return false; + } + } + + private static boolean isClosedQuietly(Connection connection) { + try { + return connection.isClosed(); + } catch (SQLException e) { + return true; + } + } + + private static void closeQuietly(Connection connection) { + try { + if (connection != null) { + connection.close(); + } + } catch (SQLException ignored) { + // Ignore close failures while disposing pooled connections. + } + } } } diff --git a/src/main/resources/messages/DataPivotBundle.properties b/src/main/resources/messages/DataPivotBundle.properties index 374c89b..8fe4cc5 100644 --- a/src/main/resources/messages/DataPivotBundle.properties +++ b/src/main/resources/messages/DataPivotBundle.properties @@ -11,6 +11,8 @@ data.pivot.notice.setting.null.action = \u6253\u5F00 data-pivot \u914D\u7F6E\u98 data.pivot.notice.connection.null = {0} \u6570\u636E\u5E93\u8FDE\u63A5\u5F02\u5E38,\u8BF7\u68C0\u67E5 DataGrip \u8BE5\u6570\u636E\u6E90\u8FDE\u63A5\u60C5\u51B5\u540E\u5237\u65B0 data.pivot.notice.connection.driver.null = {0} \u6570\u636E\u5E93\u9A71\u52A8\u4E0D\u5B58\u5728 data.pivot.notice.rom.data.error = \u52A0\u8F7D{0}\u6570\u636E\u5931\u8D25,\u5F02\u5E38\u4FE1\u606F{1} +data.pivot.driver.not.downloaded = {0} \u9A71\u52A8\u5C1A\u672A\u4E0B\u8F7D,\u8BF7\u5148\u5728 DataGrip \u6570\u636E\u6E90\u8BBE\u7F6E\u4E2D\u4E0B\u8F7D\u8BE5\u6570\u636E\u5E93\u9A71\u52A8\u540E\u91CD\u8BD5 +data.pivot.driver.register.fail = {0} \u9A71\u52A8\u6CE8\u518C\u5931\u8D25:{1} data.pivot.dialog.setting.repeat = {0} \u5DF2\u5B58\u5728\u5BF9\u5E94\u7684 mapping ,\u4E0D\u53EF\u4EE5\u91CD\u590D data.pivot.dialog.setting.module = module diff --git a/src/main/resources/messages/DataPivotBundle_en_US.properties b/src/main/resources/messages/DataPivotBundle_en_US.properties index bdcbcef..f772e77 100644 --- a/src/main/resources/messages/DataPivotBundle_en_US.properties +++ b/src/main/resources/messages/DataPivotBundle_en_US.properties @@ -11,6 +11,8 @@ data.pivot.notice.setting.null.action = open the data-pivot configuration page data.pivot.notice.connection.null = {0} The database connection is abnormal, check the DataGrip connection status of the data source and refresh it data.pivot.notice.connection.driver.null = {0} The database driver does not exist data.pivot.notice.rom.data.error = Loading {0} data failed, exception message: {1} +data.pivot.driver.not.downloaded = The database driver "{0}" has not been downloaded yet. Open the data source settings in DataGrip / Database Tools, download the driver, then try again. +data.pivot.driver.register.fail = Failed to register the database driver "{0}": {1} data.pivot.dialog.setting.repeat = {0} The corresponding mapping already exists and cannot be repeated data.pivot.dialog.setting.module = module diff --git a/src/main/resources/messages/DataPivotBundle_zh_CN.properties b/src/main/resources/messages/DataPivotBundle_zh_CN.properties index 374c89b..8fe4cc5 100644 --- a/src/main/resources/messages/DataPivotBundle_zh_CN.properties +++ b/src/main/resources/messages/DataPivotBundle_zh_CN.properties @@ -11,6 +11,8 @@ data.pivot.notice.setting.null.action = \u6253\u5F00 data-pivot \u914D\u7F6E\u98 data.pivot.notice.connection.null = {0} \u6570\u636E\u5E93\u8FDE\u63A5\u5F02\u5E38,\u8BF7\u68C0\u67E5 DataGrip \u8BE5\u6570\u636E\u6E90\u8FDE\u63A5\u60C5\u51B5\u540E\u5237\u65B0 data.pivot.notice.connection.driver.null = {0} \u6570\u636E\u5E93\u9A71\u52A8\u4E0D\u5B58\u5728 data.pivot.notice.rom.data.error = \u52A0\u8F7D{0}\u6570\u636E\u5931\u8D25,\u5F02\u5E38\u4FE1\u606F{1} +data.pivot.driver.not.downloaded = {0} \u9A71\u52A8\u5C1A\u672A\u4E0B\u8F7D,\u8BF7\u5148\u5728 DataGrip \u6570\u636E\u6E90\u8BBE\u7F6E\u4E2D\u4E0B\u8F7D\u8BE5\u6570\u636E\u5E93\u9A71\u52A8\u540E\u91CD\u8BD5 +data.pivot.driver.register.fail = {0} \u9A71\u52A8\u6CE8\u518C\u5931\u8D25:{1} data.pivot.dialog.setting.repeat = {0} \u5DF2\u5B58\u5728\u5BF9\u5E94\u7684 mapping ,\u4E0D\u53EF\u4EE5\u91CD\u590D data.pivot.dialog.setting.module = module