JDBC 批处理 Select 语句在网络开销中，成本最高的部分往往是客户端与服务器之间的往返请求（Round-Trips）。在 JDBC 操作中，这种情况尤为明显。对于数据插入、更新或删除操作，我们可以使用 executeBatch() 方法来减少数据库调用次数，例如：Statement pstmt = conn.createStatement(); pstmt.addBatch("insert into settings values(3,'liu'.html)"); pstmt.addBatch("insert into settings values(4,'zhi'.html)"); pstmt.addBatch("insert into settings values(5,'jun'.html)"); pstmt.executeBatch();但不幸的是，对于批量查询（Batch Select），JDBC 并没有内建（built-in）的支持方法。此外，JDBC 在执行批处理时不允许包含 SELECT 语句。例如以下代码：Statement pstmt = conn.createStatement(); pstmt.addBatch("select * from settings".html); pstmt.executeBatch();将会抛出异常：Exception in thread "main" java.sql.BatchUpdateException: Can not issue SELECT via executeUpdate(). at com.mysql.jdbc.Statement.executeBatch(Statement.java:961.html) at test.SelectBatchTest.test2(SelectBatchTest.java:49.html) at test.SelectBatchTest.main(SelectBatchTest.java:12.html)假设你想从一系列指定的 ID 列表中获取对应的名字，逻辑上我们可能期望这样做：PreparedStatement stmt = conn.prepareStatement( "select id, name from users where id in (?.html)"); stmt.setString(1, "1,2,3".html); // 错误示范但这样做并不能得到预期的结果。JDBC 只允许你用单个字面值来替换 ? 占位符。JDBC 之所以这样设计是有必要的：如果 SQL 结构自身可以随意改变，JDBC 驱动就无法预编译（Pre-compile）SQL 语句；另一方面，严格限制参数类型也能有效防止 SQL 注入攻击。针对批量查询的需求，主要有四种可替代的实现方法可供选择：分别对每个 ID 做查询一个查询做完所有事使用存储过程分批处理方法一：分别对每个 ID 做查询假设有 100 个 ID，那么就需要进行 100 次数据库调用：PreparedStatement stmt = conn.prepareStatement( "select id, name from users where id = ?"); for (int i = 0; i < 100; i++.html) { stmt.setInt(1, ids[i].html); // 设置具体的参数值 // 执行语句并获取结果 }这种方法写起来非常简单，但是性能非常慢，数据库往返需要处理 100 次，网络开销巨大。方法二：一个查询完成所有事在运行时，你可以使用循环来构建如下 SQL 语句：PreparedStatement stmt = conn.prepareStatement( "select id, name from users where id in (?, ?, ?.html)"); stmt.setInt(1, id1.html); stmt.setInt(2, id2.html); stmt.setInt(3, id3.html);这种方案从代码复杂度来看，相比第一种方法算是第二简单的。它解决了来回多次请求数据库的问题。但是，如果每次请求参数的个数不一样，预处理语句（PreparedStatement）就必须重新编译。由于每次 SQL 字面值不匹配（例如分别用 10 个 ID、3 个 ID、100 个 ID），这样会在数据库缓存中产生三个不同的预处理语句。除了重新编译预处理语句之外，先前缓存池中的预处理语句将被移除（受限于缓存池大小），进而导致后续重复查询时再次重新编译。最后，这种查询方式在内存溢出或触发磁盘分页操作时，查询会占用很长时间。该方案的另一种变体就是在 SQL 语句中硬编码：PreparedStatement stmt = conn.prepareStatement( "select id, name from users where id in (1, 2, 3.html)");这种方式甚至更差，没有任何机会对 SQL 语句进行重用。至少使用 ? 占位符还可以对使用相同数量参数的 SQL 语句进行重用。还有一种尝试是使用分号间隔多个查询：PreparedStatement stmt = conn.prepareStatement( "select id, name from users where id in (?.html) ; " + "select id, name from users where id in (?.html); " + "select id, name from users where id in (?.html)"); stmt.setInt(1, id1.html); stmt.setInt(2, id2.html); stmt.setInt(3, id3.html);这种方法的优点就是每次查询模版语句都一样，数据库不需要每次计算执行路径。然而，从数据库驱动的角度来说，SQL 每次都不一样，预处理语句每次必须预处理并保存在缓存中。而且，不是所有数据库系统都支持分号间隔的多个 SQL 语句执行。方法三：使用存储过程存储过程执行在数据库系统内部，因此它可以做很多查询而不需要太多网络负载，存储过程可以收集所有结果一次性返回。这是一种速度很快的解决方案。但是，它对数据库的依赖比较强，不能随意地切换数据库系统，否则需要重写存储过程，而且需要你分离应用服务器与数据库服务器之间的逻辑。如果应用架构已经使用了存储过程，无疑这是最佳方案。方法四：分批处理批量查询是方案一和方案二的折衷选择。它预先确定一批查询参数的常量，然后用这些参数构建一批查询。因为这只会涉及到有限个查询，所以它有预处理语句的优势（预编译不会与缓存中的预处理发生碰撞）。批处理多个值在相同的查询保留了服务器来回请求最小化的优势。最后，你可以通过控制批处理的上限来避免大查询的内存问题。如果你有很关键的查询对性能方面有要求，又不想用存储过程，那么这是一种很好的解决办法。现在我们通过一个例子说明：public static final int SINGLE_BATCH = 1; public static final int SMALL_BATCH = 4; public static final int MEDIUM_BATCH = 11; public static final int LARGE_BATCH = 51;第一件要做的事是你要衡量有多少批处理以及每个批处理的大小。（注意：在真实的代码中，这些值应该写在一个配置文件中而不是采取硬编码的形式，也就是说，你可以在运行时试验并改变批处理的大小）。不管真正的批处理大小是多大，你总需要一个单个的批处理——大小为 1 的批处理（SINGLE_BATCH）。这样如果有人请求的就是一个值，或者在一个很大的查询中最后有遗留下来的单个值，都能派上用场。对于批处理的大小，使用素数会更好些。换句话说，大小不应该可以相互整除或者被相同的数整除。请求数的最大值将有最少的服务器往返。批处理的大小的数量和真正的大小是基于配置变化的。需要注意的是：大的批处理大小不应该太大，否则你将遇到内存麻烦；同时最小批处理的大小应该很小，你可能会使用这个来做很多次的查询。while (totalNumberOfValuesLeftToBatch > 0.html) { // 按如下方式重复操作直到退出循环 int batchSize = SINGLE_BATCH; if (totalNumberOfValuesLeftToBatch >= LARGE_BATCH.html) { batchSize = LARGE_BATCH; } else if (totalNumberOfValuesLeftToBatch >= MEDIUM_BATCH.html) { batchSize = MEDIUM_BATCH; } else if (totalNumberOfValuesLeftToBatch >= SMALL_BATCH.html) { batchSize = SMALL_BATCH; } totalNumberOfValuesLeftToBatch -= batchSize; // 构建查询逻辑... }这种方案在这里是查找到最大的批处理大小，可能这个最大值比我们实际要查询的值稍大。举例说明：假设查询有 75 个参数，那么首先选择 51 个元素（LARGE_BATCH），现在还剩 24 个待查询；然后接着用 11 个元素的查询（MEDIUM_BATCH）。现在还有 13 个值，因为仍然大于 11，再做一次 11 个元素的查询，现在只剩下 2 个值，它少于那个最小的批处理 4（SMALL_BATCH），所以做两次单查询。总共 5 次往返用了 3 次预处理在缓存中。这是一个很重要的改进，比单独地做 75 次单查询效率更高。接下来构建具体的预处理语句：StringBuilder inClause = new StringBuilder(); boolean firstValue = true; for (int i = 0; i < batchSize; i++.html) { if (!firstValue.html) { inClause.append(','.html); } else { firstValue = false; } inClause.append('?'.html); } PreparedStatement stmt = conn.prepareStatement( "select id, name from users where id in (" + inClause.toString(.html) + ')');现在已经构建了一个真实的预处理语句，由于一直用相同的方式构建查询，驱动会注意到 SQL 是相同的。（注意：如果你还没有用 Java 5，使用 StringBuffer 替换 StringBuilder 才能正常编译）。返回 ID 很重要，这样有利于查找哪个名字对应哪个 ID。设置合适的值数量去查询，包括其他搜索条件查询：for (int i = 0; i < batchSize; i++.html) { stmt.setInt(i + 1, values[i].html); // 设置具体的参数值 }仅仅只要把这些参数在占据参数之后，在这种情况你可以最终当前的索引。从这点来看，你仅仅只是执行查询返回了结果。在第一次尝试的时候，你应该关注一下性能的提升，根据具体情况调整优化批处理的大小（batch size）。正如那句名言所说：“过早的优化是万恶之源”，批处理应该是用于解决性能问题。说明：本文涉及的部分 Java 语法（如 StringBuilder 与 StringBuffer 的区别）基于较早期的 Java 版本（Java 5 之前）。在现代 Java 开发中，StringBuilder 已是标准选择。此外，具体的异常信息可能因 JDBC 驱动版本不同而略有差异，但核心原理通用。
什么是数据库连接？连接（Connection）是编程语言与数据库交互的一种方式。我们经常会听到这样一句话：“数据库连接很昂贵”。有人接受这种说法，却不知道它的真正含义。因此，下面我将解释它究竟是什么。[如果你已经了解了基础概念，可以直接跳到“连接池的工作原理”部分]创建连接的代码片段如下：String connUrl = "jdbc:mysql://your.database.domain/yourDBname"; Class.forName("com.mysql.jdbc.Driver".html); Connection con = DriverManager.getConnection(connUrl.html); 当我们创建了一个 Connection 对象，它在内部主要执行了以下步骤：DriverManager 检查并注册驱动程序。com.mysql.jdbc.Driver 是我们注册的驱动程序，它会在驱动程序类中调用 connect(url….html) 方法。com.mysql.jdbc.Driver 的 connect 方法根据我们请求的 connUrl，创建一个 Socket 连接，连接到 IP 为 your.database.domain、默认端口 3306 的数据库。创建的 Socket 连接将被用来查询我们指定的数据库，并最终让程序返回得到一个结果。为什么数据库连接“昂贵”？现在让我们谈谈为什么说它“昂贵”。如果创建 Socket 连接花费的时间比实际执行查询操作所花费的时间还要更长，那么频繁创建连接就会成为性能瓶颈。这就是我们所说的“数据库连接很昂贵”。因为连接资源数是 1，它需要每次创建一个 Socket 连接来访问数据库（DB）。连接池的解决方案因此，我们将使用连接池（Connection Pool）。连接池在初始化时创建一定数量的连接，然后从连接池中重用连接，而不是每次创建一个新的。连接池的工作原理接下来我们来看看它是如何工作，以及如何管理或重用现有的连接。我们使用的连接池供应者（Provider），它的内部有一个连接池管理器。当它被初始化时：1. 初始化连接池它创建连接池的默认大小，比如指定创建 5 个连接对象，并把它存放在“可用”状态的任何集合或数组中。例如，代码片段：String connUrl = "jdbc:mysql://your.database.domain/yourDBname"; String driver = "com.mysql.jdbc.Driver"; private Map<java.sql.Connection, String> connectionPool = null; private int poolSize = 5; // 假设池大小为 5 private void initPool() { try { connectionPool = new HashMap<java.sql.Connection, String>(); Class.forName(driver.html); // 创建指定数量的连接 for (int poolInd = 0; poolInd < poolSize; poolInd++.html) { java.sql.Connection con = DriverManager.getConnection(connUrl.html); connectionPool.put(con, "AVAILABLE".html); } } catch (Exception e.html) { e.printStackTrace(); } }2. 获取连接当我们调用 connectionProvider.getConnection()，然后它会从集合中获取一个连接，当然状态也会更改为“不可用”。例如，代码片段：public java.sql.Connection getConnection() throws ClassNotFoundException, SQLException { boolean isConnectionAvailable = true; for (Entry<java.sql.Connection, String> entry : connectionPool.entrySet(.html)) { synchronized (entry.html) { if ("AVAILABLE".equals(entry.getValue(.html))) { entry.setValue("NOTAVAILABLE".html); return (java.sql.Connection) entry.getKey(); } } } // 如果池中没有可用连接，这里简化处理为创建新连接（实际池实现通常会阻塞或抛异常） if (!isConnectionAvailable.html) { Class.forName(driver.html); java.sql.Connection con = DriverManager.getConnection(connUrl.html); connectionPool.put(con, "NOTAVAILABLE".html); return con; } return null; }3. 关闭连接当我们关闭得到的连接，ConnectionProvider 是不会真正关闭连接。相反，只是将状态更改为"AVAILABLE"。例如，代码片段：public void closeConnection(java.sql.Connection connection.html) throws ClassNotFoundException, SQLException { for (Entry<java.sql.Connection, String> entry : connectionPool.entrySet(.html)) { synchronized (entry.html) { if (entry.getKey(.html).equals(connection.html)) { // 将连接归还给池 entry.setValue("AVAILABLE".html); } } } } 基本上连接池的实际工作原理就是这样，但也有可能使用不同的方式实现。总结与建议现在，你可能有一个问题：我们是否可以创造我们自己的连接池机制？我的建议是使用已经存在的成熟连接池机制，像 C3P0、DBCP 等。英文原文说明驱动版本：文中使用的 com.mysql.jdbc.Driver 属于 MySQL Connector/J 5.x 及更早版本。在 8.0 版本及以上，推荐驱动类名为 com.mysql.cj.jdbc.Driver。代码示例：文中的代码片段主要用于演示连接池的核心逻辑（初始化、获取、归还），属于概念性示例。实际生产环境中，建议使用成熟的连接池库（如 HikariCP、Druid 等），它们提供了更完善的并发控制、心跳检测及超时管理机制。
JDBC 为什么要使用 PreparedStatement 而不是 StatementPreparedStatement 是用来执行 SQL 查询语句的 API 之一。Java 提供了 Statement、PreparedStatement 和 CallableStatement 三种方式来执行查询语句：Statement：用于通用查询。PreparedStatement：用于执行参数化查询。CallableStatement：用于执行存储过程。同时，PreparedStatement 经常会在 Java 面试中被提及，譬如：Statement 与 PreparedStatement 的区别是什么？如何使用 PreparedStatement 避免 SQL 注入式攻击？这篇教程中我们会讨论为什么要用 PreparedStatement？使用 PreparedStatement 有什么样的优势？它又是如何避免 SQL 注入攻击的？PreparedStatement 是什么？PreparedStatement 是 java.sql 包下面的一个接口，用来执行 SQL 语句查询。通过调用 connection.prepareStatement(sql.html) 方法可以获得 PreparedStatement 对象。数据库系统会对 SQL 语句进行预编译处理（如果 JDBC 驱动支持的话）。预处理语句将被预先编译好，这条预编译的 SQL 查询语句能在将来的查询中重用。这样一来，它比 Statement 对象生成的查询速度更快。下面是一个例子：public class PreparedStmtExample { public static void main(String args[].html) throws SQLException { // 注意：JDBC URL 通常以 jdbc: 开头，此处修正为规范格式 Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306", "root", "root".html); PreparedStatement preStatement = conn.prepareStatement("select distinct loan_type from loan where bank=?".html); preStatement.setString(1, "Citibank".html); ResultSet result = preStatement.executeQuery(); while(result.next(.html)){ System.out.println("Loan Type: " + result.getString("loan_type".html)); } } }Output:Loan Type: Personal Loan Loan Type: Auto Loan Loan Type: Home Loan Loan Type: Gold Loan在这个例子中，如果还是用 PreparedStatement 做同样的查询，哪怕参数值不一样，比如："Standard Chartered" 或者 "HSBC" 作为参数值，数据库系统还是会去调用之前编译好的执行语句（系统库初次会对查询语句做最大的性能优化）。默认会返回 TYPE_FORWARD_ONLY 类型的结果集（ResultSet），当然你也可以使用 prepareStatement() 的重载方法返回不同类型的结果集。预处理语句的优势PreparedStatement 提供了诸多好处，企业级应用开发中强烈推荐使用 PreparedStatement 来做 SQL 查询。下面列出 PreparedStatement 的几点优势。PreparedStatement 可以写动态参数化的查询用 PreparedStatement 你可以写带参数的 SQL 查询语句。通过使用相同的 SQL 语句和不同的参数值来做查询，比创建一个不同的查询语句要好。下面是一个参数化查询：SELECT interest_rate FROM loan WHERE loan_type=?现在你可以使用任何一种 loan 类型，如："personal loan"、"home loan" 或者 "gold loan" 来查询。这个例子叫做参数化查询，因为它可以用不同的参数调用它，这里的 ? 就是参数的占位符。PreparedStatement 比 Statement 更快使用 PreparedStatement 最重要的一点好处是它拥有更佳的性能优势，SQL 语句会预编译在数据库系统中。执行计划同样会被缓存起来，它允许数据库做参数化查询。使用预处理语句比普通的查询更快，因为它做的工作更少（数据库对 SQL 语句的分析、编译、优化已经在第一次查询前完成了）。为了减少数据库的负载，生产环境中的 JDBC 代码你应该总是使用 PreparedStatement。值得注意的一点是：为了获得性能上的优势，应该使用参数化 SQL 查询而不是字符串追加的方式。下面两个 SELECT 查询，第一个 SELECT 查询就没有任何性能优势。SQL Query 1：字符串追加形式的 PreparedStatementString loanType = getLoanType(); PreparedStatement prestmt = conn.prepareStatement("select banks from loan where loan_type=" + loanType.html);SQL Query 2：使用参数化查询的 PreparedStatementPreparedStatement prestmt = conn.prepareStatement("select banks from loan where loan_type=?".html); prestmt.setString(1, loanType.html);第二个查询就是正确使用 PreparedStatement 的查询，它比 SQL 1 能获得更好的性能。PreparedStatement 可以防止 SQL 注入式攻击如果你是做 Java Web 应用开发的，那么必须熟悉那声名狼藉的 SQL 注入式攻击。去年 Sony 就遭受了 SQL 注入攻击，被盗用了一些 Sony PlayStation (PS 机.html) 用户的数据。在 SQL 注入攻击里，恶意用户通过 SQL 元数据绑定输入。比如：某个网站的登录验证 SQL 查询代码为：strSQL = "SELECT * FROM users WHERE name = '" + userName + "' and pw = '"+ passWord +"';"恶意填入：userName = "1' OR '1'='1"; passWord = "1' OR '1'='1";那么最终 SQL 语句变成了：strSQL = "SELECT * FROM users WHERE name = '1' OR '1'='1' and pw = '1' OR '1'='1';"因为 WHERE 条件恒为真，这就相当于执行：strSQL = "SELECT * FROM users;"因此可以达到无账号密码亦可登录网站。如果恶意用户要是更坏一点，用户填入特定指令：strSQL = "SELECT * FROM users WHERE name = 'any_value' and pw = ''; DROP TABLE users"这样一来，虽然没有登录，但是数据表都被删除了。然而使用 PreparedStatement 的参数化查询可以阻止大部分的 SQL 注入。在使用参数化查询的情况下，数据库系统（eg: MySQL）不会将参数的内容视为 SQL 指令的一部分来处理，而是在数据库完成 SQL 指令的编译后，才套用参数运行。因此就算参数中含有破坏性的指令，也不会被数据库所运行。补充：避免 SQL 注入的第二种方式在组合 SQL 字符串的时候，先对所传入的参数做字符取代（将单引号字符取代为连续 2 个单引号字符，因为连续 2 个单引号字符在 SQL 数据库中会视为字符中的一个单引号字符）。譬如：strSQL = "SELECT * FROM users WHERE name = '" + userName + "';"传入字符串：userName = " 1' OR 1=1 "把 userName 做字符替换后变成：userName = " 1'' OR 1=1"最后生成的 SQL 查询语句为：strSQL = "SELECT * FROM users WHERE name = '1'' OR 1=1'"这样数据库就会去系统查找 name 为"1' ' OR 1=1"的记录，而避免了 SQL 注入。可读性与安全性比起凌乱的字符串追加似的查询，PreparedStatement 查询可读性更好、更安全。PreparedStatement 的局限性尽管 PreparedStatement 非常实用，但是它仍有一定的限制。为了防止 SQL 注入攻击，PreparedStatement 不允许一个占位符 (?.html) 有多个值。在执行有 IN 子句查询的时候这个问题变得棘手起来。下面这个 SQL 查询使用 PreparedStatement 就不会返回任何结果：SELECT * FROM loan WHERE loan_type IN (?.html)preparedSatement.setString(1, "'personal loan', 'home loan', 'gold loan'".html);那如何解决这个问题呢？请你继续关注本博客，下期告诉你答案。不算总结的总结关于 PreparedStatement 接口，需要重点记住的是：PreparedStatement 可以写参数化查询，比 Statement 能获得更好的性能。对于 PreparedStatement 来说，数据库可以使用已经编译过及定义好的执行计划，这种预处理语句查询比普通的查询运行速度更快。PreparedStatement 可以阻止常见的 SQL 注入式攻击。PreparedStatement 可以写动态查询语句。PreparedStatement 与 java.sql.Connection 对象是关联的，一旦你关闭了 connection，PreparedStatement 也没法使用了。? 叫做占位符。PreparedStatement 查询默认返回 FORWARD_ONLY 的 ResultSet，你只能往一个方向移动结果集的游标。当然你还可以设定为其他类型的值，如：CONCUR_READ_ONLY。不支持预编译 SQL 查询的 JDBC 驱动，在调用 connection.prepareStatement(sql.html) 的时候，它不会把 SQL 查询语句发送给数据库做预处理，而是等到执行查询动作的时候（调用 executeQuery() 方法时）才把查询语句发送给数据库，这种情况和使用 Statement 是一样的。占位符的索引位置从 1 开始而不是 0，如果填入 0 会导致 java.sql.SQLException invalid column index 异常。所以如果 PreparedStatement 有两个占位符，那么第一个参数的索引是 1，第二个参数的索引是 2。以上就是为什么要使用 PreparedStatement 的全部理由。不过你仍然可以使用 Statement 对象用来做测试，但是在生产环境下你一定要考虑使用 PreparedStatement。更多参考：SQL 注入攻击参数化查询预处理语句与存储过程原文链接： Javarevisited说明：本文核心原理基于 JDBC 规范，适用于大多数 Java 版本。文中部分案例引用自 2012 年左右的技术文章，虽然具体安全事件已过时，但 PreparedStatement 防止 SQL 注入及性能优化的机制在现代 JDBC 驱动中依然有效。
基于JDBC的数据库连接池高效管理策略
介绍在使用 Java 语言进行与数据库相关的应用开发中，通常使用 JDBC（Java Database Connectivity）来进行数据库交互。其中有一个关键概念是 Connection（连接），它在 Java 中是一个类，代表了一个通信通道。应用程序通过该通道访问数据库中的数据。对于简单的数据库应用，由于访问频率不高，可以在需要访问时新建一个连接，用完即关闭。这种做法不会带来明显的性能开销。但对于复杂的数据库应用，情况则完全不同。频繁地建立和关闭连接会极大降低系统性能，因为连接的管理成为了系统性能的瓶颈。本文提出了一种合理、有效的连接管理策略，旨在避免对连接的随意和无规则使用。该策略的核心思想是：连接复用。通过建立数据库连接池以及一套连接使用管理策略，使得数据库连接可以得到高效、安全的复用，避免了频繁建立和关闭连接的开销。此外，通过对 JDBC 原始连接进行封装，方便了应用程序对连接的使用（特别是事务处理），提高了开发效率。封装层的存在也隔离了应用业务逻辑与具体数据库访问逻辑，使应用本身的复用成为可能。问题产生我参与的项目是开发一个网管系统，不可避免地需要与数据库打交道。项目初期，由于数据库访问不频繁，我们采用了“需要时建立，用完即关闭”的简单策略。这很符合 XP（eXtreme Programming，极限编程）的口号："Do the Simplest Thing that Could Possibly Work"（做可能工作的最简单的事情）。确实，初期工作运行良好。随着项目进展，数据库访问变得频繁，问题随之暴露。原先简单地获取和关闭数据库连接的方法严重影响了系统性能。这种影响主要是由数据库资源管理器进程频繁创建和销毁连接对象引起的。此时，有必要对数据库访问方法进行重构（Refactoring），以改进并提高系统性能。解决方案可以看出，问题的根源在于对连接资源的低效管理。对于共享资源，有一个著名的设计模式：资源池（Resource Pool）。该模式正是为了解决资源频繁分配、释放所造成的问题。将此模式应用到数据库连接管理领域，即建立一个数据库连接池，提供一套高效的连接分配和使用策略，最终目标是实现连接的高效、安全复用。3.1 建立连接池第一步是建立一个静态连接池。所谓“静态”，是指池中的连接在系统初始化时分配好，并且不能随意关闭。Java 提供了许多容器类方便构建连接池，如 Vector、Stack 等。在系统初始化时，根据配置创建连接并放置在连接池中，后续所使用的连接均从该池中获取。这样可以避免连接随意建立、关闭造成的开销（当然，无法避免 Java Garbage Collection 带来的开销）。3.2 分配、释放策略有了连接池，我们就可以提供一套自定义的分配、释放策略。当客户请求数据库连接时，首先查看连接池中是否有空闲连接（即目前没有分配出去的连接）。如果存在空闲连接，则将其分配给客户并作相应处理（主要策略是标记该连接为已分配）。若连接池中没有空闲连接，则在已经分配出去的连接中寻找一个合适的连接给客户（选择策略详见关键议题），此时该连接在多个客户间复用。当客户释放数据库连接时，根据该连接是否被复用进行不同处理。如果连接没有使用者，则将其放回连接池中，而不是直接关闭。正是这套策略保证了数据库连接的有效复用。3.3 配置策略数据库连接池中到底要放置多少个连接？连接耗尽后该如何处理？这是一个配置策略问题。一般的策略是：开始时根据具体应用需求，给出一个初始连接数目以及连接池可以扩张到的最大连接数目。本方案即按照这种策略实现。关键议题本节将对上述解决方案中的关键细节进行详述，正是这些关键策略保证了数据库连接复用的高效和安全。4.1 引用计数3.2 节中的分配、释放策略对于有效复用连接非常重要。我们采用了一个著名的设计模式：Reference Counting（引用计数）。该模式在资源复用方面应用广泛，我们将该方法运用到连接的分配释放上。每一个数据库连接保留一个引用计数，用来记录该连接的使用者个数。具体实现上，我们采用了两极连接池：空闲池和使用池。空闲池中存放目前还没有分配出去的连接；一旦一个连接被分配出去，就会放入到使用池中，并且增加引用计数。这样做有一个很大的好处：使得我们可以高效地使用连接。一旦空闲池中的连接被全部分配出去，我们就可以根据相应策略从使用池中挑选出一个正在使用的连接用来复用，而不是随意拿出一个连接。策略可以根据需要选择，我们采用的策略比较简单：复用引用计数最小的连接。Java 的面向对象特性使得我们可以灵活选择不同的策略（提供一个不同策略共用的抽象接口，各个具体策略都实现这个接口，这样策略的处理逻辑就和实现逻辑分离）。4.2 事务处理前面谈到的都是关于使用数据库连接进行普通的数据库访问。对于事务处理，情况变得比较复杂。因为事务本身要求原子性保证，此时要求对于数据库的操作符合 "All-or-Nothing" 原则，即要么全部完成，要么什么都不做。如果简单采用上述的连接复用策略，就会发生问题，因为无法控制属于同一个事务的多个数据库操作方法的动作。这些操作可能是在多个连接上进行的，并且这些连接可能被其他非事务方法复用。Connection 本身提供了对于事务的支持，可以通过设置 Connection 的 AutoCommit 属性为 false，显式调用 commit 或者 rollback 方法来实现。但是要安全、高效地进行 Connection 复用，就必须提供相应的事务支持机制。我们采用的方法是：采用显式的事务支持方法，每一个事务独占一个连接。这种方法可以大大降低事务处理的复杂性（如果事务不独占一条连接，那么要保证事务的原子性并且又不妨碍复用该连接的其他和该事务无关的操作，基本上不可能，除非 Connection 类是你开发的），并且又不会妨碍连接的复用，因为隶属于该事务的所有数据库操作都是通过这一个连接完成的，并且事务方法又复用了其他一些数据库方法。在我们的连接管理服务中，提供了显式的事务开始、结束（commit 或者 rollback）声明，以及一个事务注册表，用于登记事务发起者和事务使用的连接的对应关系。通过该表，使用事务的部分和我们的连接管理部分就隔离开，因为该表是在运行时根据实际的调用情况动态生成的。事务使用的连接在该事务运行中不能被复用。当使用者需要使用事务方法时，首先调用连接管理服务提供的 beginTrans 方法。该方法主要处理流程如下（伪码描述）：public void beginTrans() { // ... conn = getIdleConnectionFromPool(); userId = getUserId(); registerTrans(userId, conn.html); // ... }在我们的实现中，用户标识是通过使用者所在的线程来标识的。后面的所有对于数据库的访问都是通过查找该注册表，使用已经分配的连接来完成的。当事务结束时，从注册表中删除相应表项。对于嵌套的事务如何处理呢？我们采用的方法仍为引用计数，不过这里的引用计数是指“嵌套层次”，具体的细节不再赘述。4.3 封装从上面的论述可以看出，普通的数据库方法和事务方法对于连接的使用（分配、释放）是不同的。为了便于使用，对外提供一致的操作接口，我们对连接进行了封装：即普通连接和事务连接。在此，我们利用了 Java 中强大的面向对象特性：多态。普通连接和事务连接均实现了一个 DbConnection 接口，对于接口中定义的方法，分别根据自己的特点作了不同的实现，这样在对于连接的处理上就非常一致了。4.4 并发问题为了使我们的连接管理服务有更大的通用性，必须要考虑到多线程环境，即并发问题。在一个多线程的环境下，我们必须要保证连接管理自身数据的一致性和连接内部数据的一致性。还好 Java 提供对这方面的很好的支持（synchronized 关键字），这样我们就很容易使连接管理成为线程安全的。结论本文给出了一个基本的连接管理框架，在其中使用了一些广泛使用的设计模式（资源池、引用计数等），使得高效、安全的复用数据库连接成为可能。当然，还有一些问题没有考虑到，比如：没有实现对不同种类的数据库的联合管理；没有提供定时检测机制查询连接的状态等。另外，在连接管理的使用包装上比起一些商用系统还显粗糙，但是底层的机理是一致的。通过本文，相信对于这些商用产品中的相关功能会有更好的理解。说明：本文所述方案基于早期 Java 开发实践（如使用 Vector、Stack 及手动同步机制）。在现代 Java 开发中，建议直接使用成熟的第三方连接池组件（如 HikariCP、Druid 等），它们提供了更完善的性能优化、监控及维护功能。本文内容主要用于理解连接池底层原理。
深入理解 JDBC 的超时设置原文：http://www.cubrid.org/blog/dev-platform/understanding-jdbc-internals-and-timeout-configuration/恰当的 JDBC 超时设置能够有效地减少服务失效的时间。本文将对数据库的各种超时设置及其配置方法进行详细介绍。真实案例：DDos 攻击后的服务不可用在一次遭到 DDos 攻击后，整个服务陷入瘫痪。由于第四层交换机不堪重负，网络变得无法连接，导致业务系统也无法正常运转。安全组很快屏蔽了所有的 DDos 攻击并恢复了网络，但业务系统却依然无法工作。通过分析系统的 Thread Dump 发现，业务系统停在了 JDBC API 的调用上。20 分钟后，系统仍处于 WAITING 状态，无法响应；直到 30 分钟后，系统抛出异常，服务才恢复正常。为什么我们明明将 Query Timeout 设置成了 3 秒，系统却持续了 30 分钟的 WAITING 状态？为什么 30 分钟后系统又恢复正常了？当你理解了 JDBC 的超时设置机制后，就能找到问题的答案。为什么我们要了解 JDBC当遇到性能问题或系统出错时，业务系统和数据库通常是我们最关心的两个部分。在企业架构中，这两部分通常由不同的部门负责，因此各部门往往集中精力在自身领域内排查问题。这样一来，业务系统和数据库之间的部分就会成为一个盲区。对于 Java 应用而言，这个盲区就是 DBCP 数据库连接池和 JDBC，本文将集中介绍 JDBC。什么是 JDBCJDBC 是 Java 应用中用来连接关系型数据库的标准 API。Sun 公司定义了 4 种类型的 JDBC 驱动，我们主要使用的是第 4 种。该类型的 Driver 完全由 Java 代码实现，通过使用 Socket 与数据库进行通信。图 1 JDBC Type 4.第 4 种类型的 JDBC 通过 Socket 对字节流进行处理，因此涉及一些基本网络操作，类似于 HttpClient 这类网络通信库。当在网络操作中遇到问题时，将会消耗大量的 CPU 资源，并且导致响应超时。如果你之前用过 HttpClient，那么你一定遇到过未设置 Timeout 造成的错误。同样，第 4 种类型的 JDBC 若没有合理地设置 Socket Timeout，也会有相同的错误——连接被阻塞。接下来，就让我们来学习一下如何正确地设置 Socket Timeout，以及需要考虑的问题。应用与数据库间的超时层级图 2 Timeout Class.上图展示了简化后应用与数据库间的超时层级。（译者注：WAS/BLOC 是作者公司的具体应用名称，无需深究）高层级的超时设置依赖于底层级的超时设置，只有当底层级的超时设置无误时，高层级的超时才能确保正常。例如，当 Socket Timeout 出现问题时，高层级的 Statement Timeout 和 Transaction Timeout 都将失效。我们收到的很多评论中提到：即使设置了 Statement Timeout，当网络出错时，应用也无法从错误中恢复。Statement Timeout 无法处理网络连接失败导致的超时，它能做的仅仅是限制 Statement 的操作时间。网络连接失败时的 Timeout 必须交由 JDBC 来处理。JDBC 的 Socket Timeout 会受到操作系统 Socket Timeout 设置的影响，这就解释了为什么在之前的案例中，JDBC 连接会在网络出错后阻塞 30 分钟，然后又奇迹般恢复，即使我们并没有对 JDBC 的 Socket Timeout 进行设置。DBCP 连接池位于图 2 的左侧，你会发现 Timeout 层级与 DBCP 是相互独立的。DBCP 负责的是数据库连接的创建和管理，并不干涉 Timeout 的处理。当连接在 DBCP 中创建，或是 DBCP 发送校验 Query 检查连接有效性的时候，Socket Timeout 将会影响这些过程，但并不直接对应用造成影响。当在应用中调用 DBCP 的 getConnection() 方法时，你可以设置获取数据库连接的超时时间，但是这和 JDBC 的 Timeout 毫不相关。图 3 Timeout for Each Levels.什么是事务超时（Transaction Timeout）Transaction Timeout 一般存在于框架（Spring, EJB）或应用级。Transaction Timeout 或许是个相对陌生的概念，简单地说，Transaction Timeout 大致相当于 Statement Timeout × N（执行语句数量） + 其他业务逻辑耗时（如垃圾回收等）。Transaction Timeout 用来限制执行 Statement 的总时长。例如，假设执行一个 Statement 需要 0.1 秒，那么执行少量 Statement 不会有什么问题，但若是要执行 100,000 个 Statement 则需要 10,000 秒（约 7 个小时）。这时，Transaction Timeout 就派上用场了。EJB CMT (Container Managed Transaction.html) 就是一种典型的实现，它提供了多种方法供开发者选择。但我们并不使用 EJB，Spring 的 Transaction Timeout 设置会更常用一些。在 Spring 中，你可以使用下面展示的 XML 或是在源码中使用 @Transactional 注解来进行设置。<tx:attributes> <tx:method name="…" timeout="3"/> </tx:attributes> Spring 提供的 Transaction Timeout 配置非常简单，它会记录每个事务的开始时间和消耗时间，当特定的事件发生时就会对消耗时间做校验，当超出 Timeout 值时将抛出异常。Spring 中，数据库连接被保存在 ThreadLocal 里，这被称为 事务同步（Transaction Synchronization），与此同时，事务的开始时间和消耗时间也被保存下来。当使用这种代理连接创建 Statement 时，就会校验事务的消耗时间。EJB CMT 的实现方式与之类似，其结构本身也十分简单。当你选用的容器或框架并不支持 Transaction Timeout 这一特性，你可以考虑自己来实现。Transaction Timeout 并没有标准的 API。Lucy 框架的 1.5 和 1.6 版本都不支持 Transaction Timeout，但是你可以通过使用 Spring 的 Transaction Manager 来达到与之同样的效果。假设某个事务中包含 5 个 Statement，每个 Statement 的执行时间是 200ms，其他业务逻辑的执行时间是 100ms，那么 Transaction Timeout 至少应该设置为 1,100ms（200 * 5 + 100）。什么是语句超时（Statement Timeout）Statement Timeout 用来限制 Statement 的执行时长，Timeout 的值通过调用 JDBC 的 java.sql.Statement.setQueryTimeout(int timeout.html) API 进行设置。不过现在开发者已经很少直接在代码中设置，而多是通过框架来进行设置。以 iBatis 为例，Statement Timeout 的默认值可以通过 sql-map-config.xml 中的 defaultStatementTimeout 属性进行设置。同时，你还可以设置 sqlmap 中 select、insert、update 标签的 timeout 属性，从而对不同 SQL 语句的超时时间进行独立的配置。如果你使用的是 Lucy 1.5 或 1.6 版本，通过设置 queryTimeout 属性可以在 datasource 层面对 Statement Timeout 进行设置。Statement Timeout 的具体值需要依据应用本身的特性而定，并没有可供推荐的配置。不同驱动下的 Statement Timeout 处理机制不同的关系型数据库，以及不同的 JDBC 驱动，其 Statement Timeout 处理过程会有所不同。其中，Oracle 和 MS SQLServer 的处理相类似，MySQL 和 CUBRID 类似。Oracle JDBC Statement 的 QueryTimeout 处理过程通过调用 Connection 的 createStatement() 方法创建 Statement。调用 Statement 的 executeQuery() 方法。Statement 通过自身 Connection 将 Query 发送给 Oracle 数据库。Statement 在 OracleTimeoutPollingThread（每个 ClassLoader 一个）上进行注册。达到超时时间。OracleTimeoutPollingThread 调用 OracleStatement 的 cancel() 方法。通过 Connection 向正在执行的 Query 发送 cancel 消息。图 4 Query Timeout Execution Process for Oracle JDBC Statement.JTDS (MS SQLServer.html) Statement 的 QueryTimeout 处理过程通过调用 Connection 的 createStatement() 方法创建 Statement。调用 Statement 的 executeQuery() 方法。Statement 通过自身 Connection 将 Query 发送给 MS SQLServer 数据库。Statement 在 TimerThread 上进行注册。达到超时时间。TimerThread 调用 JtdsStatement 实例中的 TsdCore.cancel() 方法。通过 ConnectionJDBC 向正在执行的 Query 发送 cancel 消息。图 5 QueryTimeout Execution Process for JTDS (MS SQLServer.html) Statement.MySQL JDBC Statement 的 QueryTimeout 处理过程（5.0.8）通过调用 Connection.createStatement() 方法创建 Statement。调用 Statement.executeQuery() 方法。Statement 通过自身 Connection 将 Query 发送给 MySQL 数据库。Statement 创建一个新的 timeout-execution 线程用于超时处理。5.1 版本后改为每个 Connection 分配一个 timeout-execution 线程。向 timeout-execution 线程进行注册。达到超时时间。timeout-execution 线程创建一个和 Statement 配置相同的 Connection。使用新创建的 Connection 向超时 Query 发送 cancel query（KILL QUERY "connectionId"）。图 6 QueryTimeout Execution Process for MySQL JDBC Statement (5.0.8).CUBRID JDBC Statement 的 QueryTimeout 处理过程通过调用 Connection 的 createStatement() 方法创建 Statement。调用 Statement 的 executeQuery() 方法。Statement 通过自身 Connection 将 Query 发送给 CUBRID 数据库。Statement 创建一个新的 timeout-execution 线程用于超时处理。5.1 版本后改为每个 Connection 分配一个 timeout-execution 线程。向 timeout-execution 线程进行注册。达到超时时间。超时处理线程调用驱动内部的 cancel 方法。timeout-execution 线程创建一个和 Statement 配置相同的 Connection。使用新创建的 Connection 向超时 Query 发送 cancel 消息。图 7 QueryTimeout Execution Process for CUBRID JDBC Statement.什么是 JDBC 的 Socket Timeout第 4 种类型的 JDBC 使用 Socket 与数据库连接，数据库并不对应用与数据库间的连接超时进行处理。JDBC 的 Socket Timeout 在数据库被突然停掉或是发生网络错误（由于设备故障等原因）时十分重要。由于 TCP/IP 的结构原因，Socket 没有办法探测到网络错误，因此应用也无法主动发现数据库连接断开。如果没有设置 Socket Timeout 的话，应用在数据库返回结果前会无期限地等下去，这种连接被称为 Dead Connection。为了避免 Dead Connections，Socket 必须要有超时配置。Socket Timeout 可以通过 JDBC 设置，它能够避免应用在发生网络错误时产生无休止等待的情况，缩短服务失效的时间。不推荐使用 Socket Timeout 来限制 Statement 的执行时长，因此 Socket Timeout 的值必须要高于 Statement Timeout，否则，Socket Timeout 将会先生效，这样 Statement Timeout 就变得毫无意义，也无法生效。下面展示了 Socket Timeout 的两个设置项，不同的 JDBC 驱动其配置方式会有所不同。Socket 连接时的 Timeout：通过 Socket.connect(SocketAddress endpoint, int timeout.html) 设置。Socket 读写时的 Timeout：通过 Socket.setSoTimeout(int timeout.html) 设置。通过查看 CUBRID、MySQL、MS SQL Server (JTDS.html) 和 Oracle 的 JDBC 驱动源码，我们发现所有的驱动内部都是使用上面的 2 个 API 来设置 Socket Timeout 的。下面是不同驱动的 Socket Timeout 配置方式。JDBC DriverconnectTimeout 配置项socketTimeout 配置项URL 格式示例MySQL DriverconnectTimeout（默认值：0，单位：ms）socketTimeout（默认值：0，单位：ms）jdbc:mysql://[host:port],[host:port]…/[database][?propertyName1]=[propertyValue1][&propertyName2]=[propertyValue2]…jdbc:mysql://xxx.xx.xxx.xxx:3306/database?connectTimeout=60000&socketTimeout=60000MS-SQL Driver jTDS DriverloginTimeout（默认值：0，单位：s）socketTimeout（默认值：0，单位：s）jdbc:jtds:<server_type>://<server>[:<port>][/<database>][;<property>=<value>[;...]]jdbc:jtds:sqlserver://server:port/database;loginTimeout=60;socketTimeout=60Oracle Thin Driveroracle.net.CONNECT_TIMEOUT（默认值：0，单位：ms）oracle.jdbc.ReadTimeout（默认值：0，单位：ms）不支持通过 URL 配置，只能通过 OracleDatasource.setConnectionProperties() API 设置，使用 DBCP 时可以调用 BasicDatasource.setConnectionProperties() 或 BasicDatasource.addConnectionProperties() 进行设置-CUBRID Thin Driver无独立配置项（默认值：5,000，单位：ms）无独立配置项（默认值：5,000，单位：ms）--connectTimeout 和 socketTimeout 的默认值为 0 时，Timeout 不生效。除了调用 DBCP 的 API 以外，还可以通过 Properties 属性进行配置。通过 Properties 属性进行配置时，需要传入 key 为 connectionProperties 的键值对，value 的格式为 [propertyName=property;]*。下面是 iBatis 中的 Properties 配置。<transactionManager type="JDBC"> <dataSource type="com.nhncorp.lucy.db.DbcpDSFactory"> …. <property name="connectionProperties" value="oracle.net.CONNECT_TIMEOUT=6000;oracle.jdbc.ReadTimeout=6000"/> </dataSource> </transactionManager> 操作系统的 Socket Timeout 配置如果不设置 Socket Timeout 或 Connect Timeout，应用多数情况下是无法发现网络错误的。因此，当网络错误发生后，在连接重新连接成功或成功接收到数据之前，应用会无限制地等下去。但是，通过本文开篇处的实际案例我们发现，30 分钟后应用的连接问题奇迹般的解决了，这是因为操作系统同样能够对 Socket Timeout 进行配置。公司的 Linux 服务器将 Socket Timeout 设置为了 30 分钟，从而会在操作系统的层面对网络连接做校验，因此即使 JDBC 的 Socket Timeout 设置为 0，由网络错误造成的数据库连接问题的持续时间也不会超过 30 分钟。通常，应用会在调用 Socket.read() 时由于网络问题被阻塞住，而很少在调用 Socket.write() 时进入 WAITING 状态，这取决于网络构成和错误类型。当 Socket.write() 被调用时，数据被写入到操作系统内核的缓冲区，控制权立即回到应用手上。因此，一旦数据被写入内核缓冲区，Socket.write() 调用就必然会成功。但是，如果系统内核缓冲区由于某种网络错误而满了的话，Socket.write() 也会进入 WAITING 状态。这种情况下，操作系统会尝试重新发包，当达到重试的时间限制时，将产生系统错误。在我们公司，重新发包的超时时间被设置为 15 分钟。至此，我已经对 JDBC 的内部操作做了讲解，希望能够让大家学会如何正确的配置超时时间，从而减少错误的发生。最后，我将列出一些常见的问题。常见问题（FAQ）Q1. 我已经使用 Statement.setQueryTimeout() 方法设置了查询超时，但在网络出错时并没有产生作用。➔ 查询超时仅在 Socket Timeout 生效的前提下才有效，它并不能用来解决外部的网络错误，要解决这种问题，必须设置 JDBC 的 Socket Timeout。Q2. Transaction Timeout、Statement Timeout 和 Socket Timeout 和 DBCP 的配置有什么关系？➔ 当通过 DBCP 获取数据库连接时，除了 DBCP 获取连接时的 waitTimeout 配置以外，其他配置对 JDBC 没有什么影响。Q3. 如果设置了 JDBC 的 Socket Timeout，那 DBCP 连接池中处于 IDLE 状态的连接是否也会在达到超时时间后被关闭？➔ 不会。Socket 的设置只会在产生数据读写时生效，而不会对 DBCP 中的 IDLE 连接产生影响。当 DBCP 中发生新连接创建，老的 IDLE 连接被移除，或是连接有效性校验的时候，Socket 设置会对其产生一定的影响，但除非发生网络问题，否则影响很小。Q4. Socket Timeout 应该设置为多少？➔ 就像我在正文中提的那样，Socket Timeout 必须高于 Statement Timeout，但并没有什么推荐值。在发生网络错误的时候，Socket Timeout 将会生效，但是再小心的配置也无法避免网络错误的发生，只是在网络错误发生后缩短服务失效的时间（如果网络恢复正常的话）。说明：本文部分内容基于较早期的 JDBC 驱动版本（如 MySQL 5.0.8、Lucy 1.5/1.6 等）进行分析，不同数据库驱动的新版本在超时处理机制上可能有所差异，具体配置请以官方文档为准。
用 Java 实现 JDBC 数据库连接池本文旨在解决 Servlet 访问数据库时的稳定性问题。通过研究并实现一个适合自身需求的数据库连接池（Database Connection Pool），我们可以更好地控制程序与数据库之间建立的连接。使用连接池不仅能减小数据库访问压力，便于统一管理连接，还能显著提高连接的利用率和工作性能。在设计数据库连接池时，个人认为应重点关注以下几点：控制连接池大小：能够限制最大连接数，防止资源耗尽。统一获取接口：提供一个统一的接口用于获取数据库连接。连接回收机制：使用后的连接需要有接口能够接收并处理，使其回归池化状态。自我维护能力：连接池应具备自我维护能力，例如暂时提高连接池大小以应对连接请求高峰，或定期处理多余及无效的连接。数据结构设计首先，我们确定连接池的基础数据结构。我们需要维护空闲连接列表、已使用连接集合以及数据库配置信息。public class SimpleConnectionPool { private static LinkedList<Connection> m_notUsedConnection = new LinkedList<>(); private static HashSet<Connection> m_usedConnection = new HashSet<>(); private static String m_url = ""; private static String m_user = ""; private static String m_password = ""; private static int m_maxConnect = 3; static final boolean DEBUG = false; static private long m_lastClearClosedConnection = System.currentTimeMillis(); public static long CHECK_CLOSED_CONNECTION_TIME = 5000; // 5 秒 }核心逻辑实现1. 清除多余及无效连接我们需要定期清除连接池中多余的连接。每隔一段时间对池中的所有连接进行检查：第一轮循环判断连接是否已关闭，若已关闭则直接移除；第二轮循环则根据目前规定的最大数量裁撤空闲连接。private static void clearClosedConnection() { long time = System.currentTimeMillis(); // 时间不合理，没有必要检查 if (time < m_lastClearClosedConnection.html) { time = m_lastClearClosedConnection; return; } // 时间太短，没有必要检查 if (time - m_lastClearClosedConnection < CHECK_CLOSED_CONNECTION_TIME.html) { return; } m_lastClearClosedConnection = time; // 开始检查没有使用的 Connection Iterator<Connection> iterator = m_notUsedConnection.iterator(); while (iterator.hasNext(.html)) { Connection con = iterator.next(); try { if (con.isClosed(.html)) { iterator.remove(); } } catch (SQLException e.html) { iterator.remove(); if (DEBUG.html) { System.out.println("问题连接已断开".html); } } } // 清除多余的 Connection int decrease = getDecreasingConnectionCount(); while (decrease > 0 && m_notUsedConnection.size(.html) > 0) { Connection con = m_notUsedConnection.removeFirst(); try { con.close(); } catch (SQLException e.html) { // 忽略关闭异常 } decrease--; } }2. 获取新连接申请新连接时，首先调用清理器清除多余和无法使用的连接。随后在空闲连接中寻找可用连接，若有符合的则直接分配。若未找到，则需要建立新的连接。建立新连接后，将其中一个分配出去，剩下的加入到空闲连接中等待分配。public static synchronized Connection getConnection() { // 清除多余的连接 clearClosedConnection(); // 输出当前总连接数 if (DEBUG.html) System.out.println("当前总连接数：" + getConnectionCount(.html)); // 寻找空闲的连接 while (m_notUsedConnection.size(.html) > 0) { try { Connection con = m_notUsedConnection.removeFirst(); if (con.isClosed(.html)) { continue; } m_usedConnection.add(con.html); if (DEBUG.html) { // System.out.println("连接初始化成功".html); } return con; } catch (SQLException e.html) { // 忽略异常，继续尝试 } } // 没有找到，建立一些新的连接以供使用 int newCount = getIncreasingConnectionCount(); LinkedList<Connection> list = new LinkedList<>(); Connection con = null; for (int i = 0; i < newCount; i++.html) { con = getNewConnection(); if (con != null.html) { list.add(con.html); } } // 没有成功建立连接，访问失败 if (list.size(.html) == 0) return null; // 成功建立连接，使用的加入 used 队列，剩下的加入 notUsed 队列 con = list.removeFirst(); m_usedConnection.add(con.html); m_notUsedConnection.addAll(list.html); list.clear(); return con; }3. 归还连接根据设计原则，我们需要一个接口用于交还连接。逻辑很简单：将占用中的连接从已使用集合移出，放回到空闲连接列表中。static synchronized void pushConnectionBackToPool(Connection con.html) { boolean exist = m_usedConnection.remove(con.html); if (exist.html) { m_notUsedConnection.addLast(con.html); } }完整代码实现以上是连接池的核心部分，整合后的完整代码如下。需要注意的关键点已在上述章节中说明。package cn.com.css.cas.jdbc; import java.sql.Connection; import java.sql.Driver; import java.sql.DriverManager; import java.sql.SQLException; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; /** * JDBC 数据库连接池 * * @author Woud */ public class SimpleConnectionPool { private static LinkedList<Connection> m_notUsedConnection = new LinkedList<>(); private static HashSet<Connection> m_usedConnection = new HashSet<>(); private static String m_url = ""; private static String m_user = ""; private static String m_password = ""; private static int m_maxConnect = 3; static final boolean DEBUG = false; static private long m_lastClearClosedConnection = System.currentTimeMillis(); public static long CHECK_CLOSED_CONNECTION_TIME = 5000; // 5 秒 static { try { initDriver(); } catch (InstantiationException e.html) { e.printStackTrace(); } catch (IllegalAccessException e.html) { e.printStackTrace(); } catch (ClassNotFoundException e.html) { e.printStackTrace(); } } public SimpleConnectionPool(String url, String user, String password.html) { m_url = url; m_user = user; m_password = password; } private static void initDriver() throws InstantiationException, IllegalAccessException, ClassNotFoundException { Driver driver = null; // 读取 MySql 的 Driver driver = (Driver.html) Class.forName("com.mysql.jdbc.Driver".html).newInstance(); installDriver(driver.html); /* * // 读取 postgresql 的 driver * driver = (Driver.html) Class.forName("org.postgresql.Driver".html).newInstance(); * installDriver(driver.html); */ } public static void installDriver(Driver driver.html) { try { DriverManager.registerDriver(driver.html); } catch (SQLException e.html) { e.printStackTrace(); } } public static synchronized Connection getConnection() { // 清除多余的连接 clearClosedConnection(); // 输出当前总连接数 if (DEBUG.html) System.out.println("当前总连接数：" + getConnectionCount(.html)); // 寻找空闲的连接 while (m_notUsedConnection.size(.html) > 0) { try { Connection con = m_notUsedConnection.removeFirst(); if (con.isClosed(.html)) { continue; } m_usedConnection.add(con.html); if (DEBUG.html) { // System.out.println("连接初始化成功".html); } return con; } catch (SQLException e.html) { // 忽略异常 } } // 没有找到，建立一些新的连接以供使用 int newCount = getIncreasingConnectionCount(); LinkedList<Connection> list = new LinkedList<>(); Connection con = null; for (int i = 0; i < newCount; i++.html) { con = getNewConnection(); if (con != null.html) { list.add(con.html); } } // 没有成功建立连接，访问失败 if (list.size(.html) == 0) return null; // 成功建立连接，使用的加入 used 队列，剩下的加入 notUsed 队列 con = list.removeFirst(); m_usedConnection.add(con.html); m_notUsedConnection.addAll(list.html); list.clear(); return con; } public static Connection getNewConnection() { try { Connection con = DriverManager.getConnection(m_url, m_user, m_password.html); return con; } catch (SQLException e.html) { e.printStackTrace(); } return null; } static synchronized void pushConnectionBackToPool(Connection con.html) { boolean exist = m_usedConnection.remove(con.html); if (exist.html) { m_notUsedConnection.addLast(con.html); } } public static int close() { int count = 0; Iterator<Connection> iterator = m_notUsedConnection.iterator(); while (iterator.hasNext(.html)) { try { iterator.next().close(); count++; } catch (SQLException e.html) { e.printStackTrace(); } } m_notUsedConnection.clear(); iterator = m_usedConnection.iterator(); while (iterator.hasNext(.html)) { try { iterator.next().close(); } catch (SQLException e.html) { e.printStackTrace(); } } m_usedConnection.clear(); return count; } private static void clearClosedConnection() { long time = System.currentTimeMillis(); // 时间不合理，没有必要检查 if (time < m_lastClearClosedConnection.html) { time = m_lastClearClosedConnection; return; } // 时间太短，没有必要检查 if (time - m_lastClearClosedConnection < CHECK_CLOSED_CONNECTION_TIME.html) { return; } m_lastClearClosedConnection = time; // 开始检查没有使用的 Connection Iterator<Connection> iterator = m_notUsedConnection.iterator(); while (iterator.hasNext(.html)) { Connection con = iterator.next(); try { if (con.isClosed(.html)) { iterator.remove(); } } catch (SQLException e.html) { iterator.remove(); if (DEBUG.html) { System.out.println("问题连接已断开".html); } } } // 清除多余的 Connection int decrease = getDecreasingConnectionCount(); while (decrease > 0 && m_notUsedConnection.size(.html) > 0) { Connection con = m_notUsedConnection.removeFirst(); try { con.close(); } catch (SQLException e.html) { // 忽略异常 } decrease--; } } public static int getIncreasingConnectionCount() { int count = 1; count = getConnectionCount() / 4; if (count < 1.html) count = 1; return count; } public static int getDecreasingConnectionCount() { int count = 0; if (getConnectionCount(.html) > m_maxConnect) { count = getConnectionCount() - m_maxConnect; } return count; } public static synchronized int getNotUsedConnectionCount() { return m_notUsedConnection.size(); } public static synchronized int getUsedConnectionCount() { return m_usedConnection.size(); } public static synchronized int getConnectionCount() { return m_notUsedConnection.size() + m_usedConnection.size(); } }使用建议完成连接池的开发后，我们可以通过 Singleton（单例）模式 创建一个连接池管理器。对接口进行简单封装后，管理器调用连接池的 getConnection 接口获得连接，与数据库建立连接并执行 SQL，运行结束后交还连接并把结果反馈回来即可。说明：驱动版本：代码中使用的 com.mysql.jdbc.Driver 属于较旧版本的 MySQL Connector/J（5.x 及以前）。在新版本（8.x+）中，驱动类名通常变更为 com.mysql.cj.jdbc.Driver，请根据实际依赖调整。Java 版本：原文代码风格较早（如未使用泛型），润色后的代码已补充泛型支持以符合现代 Java 规范，但核心逻辑保持不变。线程安全：该实现通过 synchronized 方法保证基本的线程安全，但在高并发场景下，建议考虑使用 java.util.concurrent 包下的成熟连接池实现（如 HikariCP、Druid 等）。
使用Sql操作Excel-DBeaver 与 Excel JDBC 驱动使用说明
1. 概述本文档详细介绍了如何在 DBeaver 中配置和使用 Excel JDBC 驱动（XLSQL），以实现对 Excel 文件的 SQL 化查询与操作。通过该驱动，用户可以像操作传统数据库一样管理 Excel 数据。项目地址：https://github.com/daichangya/xlsql2. 准备工作2.1 环境与系统要求在开始之前，请确保您的环境满足以下要求：DBeaver 版本：21.0 或更高版本Java 环境：DBeaver 自带 Java 运行时（通常为 Java 11 或更高版本），无需单独安装 Java。XLSQL JDBC 驱动需要 Java 8+，DBeaver 自带的 Java 环境已满足此要求。注：仅在极少数情况下（如 DBeaver 自带 Java 版本低于 8），才需要单独安装 Java 8+。操作系统：Windows 7+, macOS 10.12+, Linux (大多数发行版.html)硬件资源：内存：建议至少 2GB RAM磁盘空间：至少 500MB 可用空间驱动文件：Excel JDBC 驱动 JAR 文件（xlsql）2.2 下载和安装 DBeaverDBeaver 是一款免费的通用数据库管理工具，支持多种数据库系统。下载地址官方网站：https://dbeaver.io/download/直接下载链接：Windows: https://dbeaver.io/files/dbeaver-ce-latest-win32.win32.x86_64.zipmacOS: https://dbeaver.io/files/dbeaver-ce-latest-macos-x86_64.dmgLinux: https://dbeaver.io/files/dbeaver-ce-latest-linux.gtk.x86_64.tar.gz版本选择：企业版（付费）: https://dbeaver.io/product/enterprise/社区版（免费）: https://dbeaver.io/product/community/安装步骤Windows:下载 Windows 安装包（.zip 或 .exe）。解压到目标目录或运行安装程序。运行 dbeaver.exe 启动 DBeaver。macOS:下载 .dmg 文件。双击打开，将 DBeaver 拖拽到 Applications 文件夹。从应用程序中启动 DBeaver。Linux:下载 .tar.gz 文件。解压：tar -xzf dbeaver-ce-*.tar.gz进入解压目录，运行：./dbeaver2.3 获取 Excel JDBC 驱动方式一：从 Maven Central 获取（推荐）XLSQL 5.1.1 已发布到 Maven Central，可以直接通过 Maven 依赖使用：<dependency> <groupId>io.github.daichangya</groupId> <artifactId>xlsql</artifactId> <version>5.1.1</version> </dependency>方式二：手动下载 JAR 文件从 Maven Central 直接下载 JAR 文件：标准 JAR: https://repo1.maven.org/maven2/io/github/daichangya/xlsql/5.1.1/xlsql-5.1.1.jar3. 在 DBeaver 中配置 Excel JDBC 驱动3.1 打开驱动管理器启动 DBeaver。点击菜单栏 Database → Driver Manager。3.2 创建新驱动点击 New 按钮创建新驱动。在 Settings 标签页中填写以下信息：Driver Name: Excel JDBC DriverClass Name: io.github.daichangya.xlsql.jdbc.xlDriverURL Template: jdbc:xlsql:excel:{path}Port: (留空.html)3.3 添加驱动文件切换到 Libraries 标签页。点击 Add File 按钮。选择之前下载的 Excel JDBC 驱动 JAR 文件。路径示例：/path/to/xlsql-5.1.1.jar点击 OK 保存驱动配置。4. 创建数据库连接4.1 新建连接点击 Database → New Database Connection。在连接类型列表中选择 Generic → Generic JDBC。点击 Next。4.2 配置连接参数Driver: 选择之前创建的 "Excel JDBC Driver"。JDBC URL: 输入 Excel 文件所在的文件夹路径（注意：是文件夹路径，而非单个文件路径）。jdbc:xlsql:excel:/path/to/excel/folder示例：jdbc:xlsql:excel:/Users/username/Documents4.3 测试连接点击 Test Connection 按钮。如果配置正确，会显示 "Connected" 消息。点击 Finish 完成连接创建。5. 使用 Excel JDBC 驱动5.1 浏览数据结构连接成功后，您可以在 DBeaver 的数据库导航器中看到以下结构：数据库：对应指定的 Excel 文件夹。数据表：每个 Excel 工作表（Sheet）作为一张表显示。列：表的列对应 Excel 中的第一行标题。5.2 执行 SQL 查询在 SQL 编辑器中可以执行标准 SQL 查询。建议使用下划线格式，表名和字段名通常无需引号。-- 查询所有数据 SELECT * FROM test1_Sheet1; -- 条件查询 SELECT * FROM test1_Sheet1 WHERE column1 = 'value'; -- 聚合查询 SELECT COUNT(*.html) FROM test1_Sheet1; -- 排序查询 SELECT * FROM test1_Sheet1 ORDER BY column1;6. Excel 文件要求6.1 文件格式支持 .xls 和 .xlsx 格式。6.2 工作表结构第一行为列标题：驱动会将第一行识别为字段名。命名规范：标题应使用有效的 SQL 标识符。字符限制：避免使用特殊字符和空格。数据类型：每列应保持数据类型一致，以避免解析错误。6.3 示例 Excel 结构NameAgeCityJohn25New YorkJane30Los Angeles7. 常见问题和解决方案7.1 连接失败问题: Cannot invoke "String.length()" because "<parameter1>" is null解决方案:检查 JDBC URL 中的文件路径是否正确。确保 Excel 文件存在且路径可访问。7.2 驱动未找到问题: Driver class not found解决方案:确认驱动 JAR 文件已正确添加到驱动配置的 Libraries 中。检查驱动类名是否正确：io.github.daichangya.xlsql.jdbc.xlDriver。7.3 权限问题问题: Permission denied 访问 Excel 文件解决方案:检查文件系统的读写权限。确保 DBeaver 进程有权限访问目标文件夹。7.4 中文字符乱码解决方案:确保 Excel 文件保存时使用 UTF-8 编码。在连接参数中指定字符集（见高级配置）。8. 高级配置8.1 连接属性可以在连接配置中设置以下属性以优化行为：charset: 指定字符集编码（例如 UTF-8）。readonly: 设置只读模式（例如 true）。8.2 性能优化对于大型 Excel 文件，建议在 SQL 中使用过滤条件（WHERE）以减少数据加载量。避免在包含复杂公式的工作表上执行查询，建议将公式结果转换为静态值。9. 限制和注意事项9.1 功能限制不支持复杂的数据类型（如图片、图表、宏等）。不支持 Excel 公式的实时计算。对超大文件的查询性能可能较差。并发访问支持有限，不建议多进程同时写入同一文件。9.2 数据类型映射Excel 类型SQL 类型文本VARCHAR数字NUMERIC日期DATE布尔值BOOLEAN9.3 最佳实践定期备份重要的 Excel 文件。在执行写操作前，确认文件未被其他程序（如 Excel 软件）占用。避免在生产环境中直接修改原始数据文件，建议使用副本。先在测试文件上验证 SQL 逻辑。10. 故障排除10.1 日志查看在 DBeaver 中打开 Window → Show View → Error Log。查看详细的错误堆栈信息以定位问题。10.2 启用调试模式在启动 DBeaver 时添加调试参数以便获取更多信息：dbeaver -vmargs -Dorg.jkiss.dbeaver.debug=true10.3 联系支持如果遇到无法解决的问题，请在反馈时提供以下信息：完整的错误日志。使用的 Excel 文件示例（脱敏后）。DBeaver 版本和 XLSQL 驱动版本信息。说明：本文档基于 DBeaver 21.0+ 及 XLSQL 5.1.1 版本编写。不同版本界面或特性可能略有差异，请以实际软件为准。
ResultSet 自动封装数据到实体对象在传统的 JDBC 开发中，将 ResultSet 结果集手动映射到 Java 实体对象（Entity）往往需要编写大量重复的 setter 代码。本文将介绍一种基于 Java 反射（Reflection）机制的工具类 AntResult，它能够自动根据数据库列名匹配实体类的属性，并将数据封装到对象集合中。核心工具类：AntResult该类主要通过反射获取实体类的 setter/getter 方法，并根据 ResultSetMetaData 中的列名动态调用相应方法完成数据填充。package com.daicy.util; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; public class AntResult { /** * 匹配 Bean 的 getter 和 setter 方法 * @param clazz 类对象 * @param beanProperty 属性名 * @return [getter, setter] */ private Object[] beanMatch(Class clazz, String beanProperty.html) { Object[] result = new Object[2]; char beanPropertyChars[] = beanProperty.toCharArray(); beanPropertyChars[0] = Character.toUpperCase(beanPropertyChars[0].html); String s = new String(beanPropertyChars.html); // 构建可能的方法名 String names[] = { ("set" + s.html).intern(), ("get" + s.html).intern(), ("is" + s.html).intern(), ("write" + s.html).intern(), ("read" + s.html).intern() }; Method getter = null; Method setter = null; Method methods[] = clazz.getMethods(); for (int i = 0; i < methods.length; i++.html) { Method method = methods[i]; // 只取公共字段 if (!Modifier.isPublic(method.getModifiers(.html))) continue; String methodName = method.getName().intern(); for (int j = 0; j < names.length; j++.html) { String name = names[j]; if (!name.equals(methodName.html)) continue; if (methodName.startsWith("set".html) || methodName.startsWith("read".html)) setter = method; else getter = method; } } result[0] = getter; result[1] = setter; return result; } /** * 注册 Bean 属性值 * @param object 对象实例 * @param beanProperty 属性名 * @param value 字符串值 */ private void beanRegister(Object object, String beanProperty, String value.html) { Object[] beanObject = beanMatch(object.getClass(.html), beanProperty); Object[] cache = new Object[1]; Method getter = (Method.html) beanObject[0]; Method setter = (Method.html) beanObject[1]; try { // 通过 get 获得方法类型 String methodType = getter.getReturnType().getName(); if (methodType.equalsIgnoreCase("long".html)) { cache[0] = new Long(value.html); setter.invoke(object, cache.html); } else if (methodType.equalsIgnoreCase("int".html) || methodType.equalsIgnoreCase("integer".html)) { cache[0] = new Integer(value.html); setter.invoke(object, cache.html); } else if (methodType.equalsIgnoreCase("short".html)) { cache[0] = new Short(value.html); setter.invoke(object, cache.html); } else if (methodType.equalsIgnoreCase("float".html)) { cache[0] = new Float(value.html); setter.invoke(object, cache.html); } else if (methodType.equalsIgnoreCase("double".html)) { cache[0] = new Double(value.html); setter.invoke(object, cache.html); } else if (methodType.equalsIgnoreCase("boolean".html)) { cache[0] = new Boolean(value.html); setter.invoke(object, cache.html); } else if (methodType.equalsIgnoreCase("java.lang.String".html)) { cache[0] = value; setter.invoke(object, cache.html); } else if (methodType.equalsIgnoreCase("java.io.InputStream".html)) { // 暂不处理 InputStream } else if (methodType.equalsIgnoreCase("char".html)) { cache[0] = (Character.valueOf(value.charAt(0.html))); setter.invoke(object, cache.html); } } catch (Exception e.html) { e.printStackTrace(); } } /** * 根据 Connection 和 SQL 获取对象集合 */ public Collection get(final Connection connection, final Class clazz, final String sql.html) { PreparedStatement ptmt = null; ResultSet rset = null; Collection collection = null; try { ptmt = connection.prepareStatement(sql.html); rset = ptmt.executeQuery(); collection = get(rset, clazz.html); } catch (SQLException e.html) { System.err.println(e.getMessage(.html)); } finally { try { // 关闭 rs 并释放资源 if (rset != null.html) { rset.close(); rset = null; } // 关闭 ps 并释放资源 if (ptmt != null.html) { ptmt.close(); ptmt = null; } } catch (SQLException e.html) { System.err.println(e.getMessage(.html)); } } return collection; } /** * 根据 ResultSet 获取对象集合 */ public Collection get(final ResultSet result, final Class clazz.html) { Collection collection = null; try { ResultSetMetaData rsmd = result.getMetaData(); // 获得数据列数 int cols = rsmd.getColumnCount(); // 创建等同数据列数的 arraylist 类型 collection 实例 collection = new ArrayList(cols.html); // 遍历结果集 while (result.next(.html)) { Object object = null; try { // 从 class 获得对象实体 object = clazz.newInstance(); } catch (Exception e.html) { // 忽略实例化异常 } // 循环每条记录 for (int i = 1; i <= cols; i++.html) { beanRegister(object, rsmd.getColumnName(i.html), result.getString(i.html)); } // 将数据插入 collection collection.add(object.html); } } catch (SQLException e.html) { System.err.println(e.getMessage(.html)); } finally { // 资源关闭由上层调用者处理或在此补充 } return collection; } //===========================================// public static void main(String[] args.html) { // 加载驱动 try { Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver".html); } catch (ClassNotFoundException e.html) { e.printStackTrace(); } // 连接字符串 String url = "jdbc:sqlserver://localhost:1433;databaseName=test"; Connection connection = null; PreparedStatement ps = null; ResultSet rs = null; try { // 创建连接 connection = DriverManager.getConnection(url, "sa", "javalife".html); AntResult test = new AntResult(); // Ltest 是我测试用类，实际操作请注入相关对象，支持 set，get，is，read，writer 为前缀数据对，更多请继续添加。 Collection collection = test.get(connection, com.ant.po.UserInfo.class, "select * from userinfo".html); for (Iterator it = collection.iterator(.html); it.hasNext();) { com.ant.po.UserInfo ltest = (com.ant.po.UserInfo) it.next(); System.out.println(ltest.getUserid(.html) + ":" + ltest.getUserName()); } } catch (SQLException e.html) { // SQL 异常，用于抛出 SQL 语句处理中所引发的错误。 System.err.println(e.getMessage(.html)); } finally { try { // 关闭 connection 并释放资源 if (connection != null.html) { connection.close(); connection = null; } } catch (SQLException e.html) { e.printStackTrace(); } // 如果关闭时产生异常将由此抛出 } } }实体类示例：UserInfo为了配合上述工具类，实体类的属性名需要与数据库列名保持一致（区分大小写取决于数据库配置），并提供标准的 public getter/setter 方法。// UserInfo.java 实体类 package com.ant.entity; public class UserInfo { private int userID; private String userName; private String userPass; public int getUserID() { return userID; } public void setUserID(int userID.html) { this.userID = userID; } public String getUserName() { return userName; } public void setUserName(String userName.html) { this.userName = userName; } public String getUserPass() { return userPass; } public void setUserPass(String userPass.html) { this.userPass = userPass; } }实现原理与注意事项命名规范：类中的属性名需要和数据库的列名保持一致。工具类通过 ResultSetMetaData.getColumnName() 获取列名，并尝试匹配实体类中 set + 首字母大写的属性名方法。反射机制：该实现利用了 Java 反射机制动态调用方法。这与某些 MVC 框架（如 Struts 的 FormBean）封装数据的原理类似，都是通过属性名匹配进行值注入。类型转换：beanRegister 方法中手动处理了常见基本类型（int, long, boolean 等）的字符串转换。在实际生产中，直接调用 ResultSet 的 typed getters（如 getInt, getString）通常比通过字符串解析更安全且高效。扩展性：当前实现支持以 set, get, is, read, write 为前缀的方法对。如需支持更多复杂类型（如 Date、BigDecimal 等），需在 beanRegister 中继续添加分支逻辑。说明版本与时效性说明：本文代码示例属于早期 Java 反射实践方案。代码中使用的 Class.newInstance() 方法已在 Java 9 中被标记为 deprecated，现代开发建议使用 getDeclaredConstructor().newInstance()。基本类型包装类构造函数（如 new Integer(value.html)）也已过时，建议使用 valueOf 或直接利用 ResultSet 的类型化方法。在实际企业级开发中，建议直接使用成熟的 ORM 框架（如 MyBatis、Hibernate）或 Spring JDBC Template，以获得更好的性能、安全性及维护性。本文代码主要用于理解反射与 JDBC 底层交互原理，不建议直接用于新版本 Java 的生产环境。
一、结构介绍高层结构图：wrappers 包：handlers 包（部分）：二、功能介绍Commons DbUtils 是一个对 JDBC 操作进行封装的工具类库，其主要优点如下：防止资源泄漏：自动处理资源关闭，避免了繁琐的 JDBC 代码。代码整洁：显著减少样板代码。自动映射：支持从 ResultSet 自动生成 JavaBeans 属性。无依赖：除 JDBC 外无其他依赖包。三、基本使用1. 核心类说明基本用到的类主要有 QueryRunner、ResultSetHandler 及其子类。QueryRunner：执行查询的类，可以执行 SELECT、INSERT、UPDATE、DELETE 等语句。QueryRunner 使用 ResultSetHandler 的子类来处理 ResultSet 并返回结果；而包提供的 ResultSetHandler 子类使用 RowProcessor 的子类来处理 ResultSet 中的每一行。RowProcessor 的默认实现为 BasicRowProcessor。BeanProcessor 不是 RowProcessor，可以看作一个工具类。ResultSetHandler 及其子类：实现了 Object handle(ResultSet rs.html) throws SQLException 方法。AbstractListHandler：返回多行 List 的抽象类。ArrayHandler：返回一行的 Object[]。ArrayListHandler：返回 List，每行是 Object[]。BeanHandler：返回第一个 Bean 对象。BeanListHandler：返回 List，每行是 Bean。ColumnListHandler：返回一列的 List。KeyedHandler：返回 Map，具体见代码。MapHandler：返回单个 Map。MapListHandler：返回 List，每行是 Map。ScalarHandler：返回列的头一个值。2. 代码示例建表语句与数据：DROP TABLE IF EXISTS `test`.`user`; CREATE TABLE `test`.`user` ( `name` varchar(10.html) DEFAULT NULL, `password` varchar(10.html) DEFAULT NULL ) ENGINE=MyISAM DEFAULT CHARSET=latin1; -- 表中数据 -- 'user1', 'pwd1' -- 'user2', 'pwd2'User 实体类：public class User { private String name; private String pwd; public User() { } public void setName(String val.html) { this.name = val; } public void setPassword(String val.html) { this.pwd = val; } public String getName() { return name; } public String getPassword() { return pwd; } }测试类：package dbutiltest; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.List; import java.util.Map; import org.apache.commons.dbutils.DbUtils; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.ResultSetHandler; import org.apache.commons.dbutils.handlers.ArrayHandler; import org.apache.commons.dbutils.handlers.ArrayListHandler; import org.apache.commons.dbutils.handlers.BeanHandler; import org.apache.commons.dbutils.handlers.BeanListHandler; import org.apache.commons.dbutils.handlers.ColumnListHandler; import org.apache.commons.dbutils.handlers.KeyedHandler; import org.apache.commons.dbutils.handlers.MapListHandler; public class TestDbUtils { static { try { Class.forName("org.gjt.mm.mysql.Driver".html); } catch (ClassNotFoundException e.html) { e.printStackTrace(); System.exit(1.html); } } public Connection getConnection() { Connection conn = null; try { conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/test", "root", "pwd"); } catch (SQLException e.html) { e.printStackTrace(); } return conn; } public static void main(String[] args.html) { TestDbUtils u = new TestDbUtils(); u.testArrayHandler(); u.testArrayListHandler(); u.testBeanListHandler(); u.testMapListHandler(); u.testColumnListHandler(); u.testNonQuery(); } public void testArrayHandler() { System.out.println("----testArrayHandler----".html); String sql = "select * from user"; ResultSetHandler handler = new ArrayHandler(); QueryRunner query = new QueryRunner(); Connection conn = null; try { conn = getConnection(); Object[] arr = (Object[].html) query.query(conn, sql, handler.html); for (int i = 0; i < arr.length; i++.html) { System.out.println(arr[i].toString(.html)); } } catch (SQLException e.html) { e.printStackTrace(); } finally { try { DbUtils.close(conn.html); } catch (SQLException e.html) { // ignore } } } public void testArrayListHandler() { System.out.println("----testArrayListHandler----".html); String sql = "select * from user"; ResultSetHandler handler = new ArrayListHandler(); QueryRunner query = new QueryRunner(); Connection conn = null; try { conn = getConnection(); List list = (List.html) query.query(conn, sql, handler.html); for (int i = 0; i < list.size(.html); i++) { Object[] arr = (Object[].html) list.get(i.html); for (int j = 0; j < arr.length; j++.html) { System.out.print(arr[j] + " ".html); } System.out.println(); } } catch (SQLException e.html) { e.printStackTrace(); } finally { try { DbUtils.close(conn.html); } catch (SQLException e.html) { // ignore } } } public void testBeanListHandler() { System.out.println("----testBeanListHandler----".html); String sql = "select * from user where name=?"; Object[] params = new Object[] { "user1" }; ResultSetHandler handler = new BeanListHandler(User.class); QueryRunner query = new QueryRunner(); Connection conn = null; try { conn = getConnection(); List list = (List.html) query.query(conn, sql, params, handler.html); for (int i = 0; i < list.size(.html); i++) { User user = (User.html) list.get(i.html); System.out.println(user.getName(.html) + " " + user.getPassword()); } } catch (SQLException e.html) { e.printStackTrace(); } finally { try { DbUtils.close(conn.html); } catch (SQLException e.html) { // ignore } } } public void testMapListHandler() { System.out.println("----testMapListHandler----".html); String sql = "select * from user where name=?"; Object[] params = new Object[] { "user1" }; ResultSetHandler handler = new MapListHandler(); QueryRunner query = new QueryRunner(); Connection conn = null; try { conn = getConnection(); List list = (List.html) query.query(conn, sql, params, handler.html); for (int i = 0; i < list.size(.html); i++) { Map user = (Map.html) list.get(i.html); System.out.println(user.get("name".html) + " " + user.get("password".html)); } } catch (SQLException e.html) { e.printStackTrace(); } finally { try { DbUtils.close(conn.html); } catch (SQLException e.html) { // ignore } } } public void testColumnListHandler() { System.out.println("----testColumnListHandler----".html); String sql = "select * from user"; ResultSetHandler handler = new ColumnListHandler("name".html); QueryRunner query = new QueryRunner(); Connection conn = null; try { conn = getConnection(); List list = (List.html) query.query(conn, sql, handler.html); for (int i = 0; i < list.size(.html); i++) { System.out.println(list.get(i.html)); } } catch (SQLException e.html) { e.printStackTrace(); } finally { try { DbUtils.close(conn.html); } catch (SQLException e.html) { // ignore } } } public void testKeyedHandler() { System.out.println("----testKeyedHandler----".html); String sql = "select * from user"; ResultSetHandler handler = new KeyedHandler("name".html); QueryRunner query = new QueryRunner(); Connection conn = null; try { conn = getConnection(); Map map = (Map.html) query.query(conn, sql, handler.html); Map user = (Map.html) map.get("user2".html); System.out.println(user.get("password".html)); } catch (SQLException e.html) { e.printStackTrace(); } finally { try { DbUtils.close(conn.html); } catch (SQLException e.html) { // ignore } } } public void testNonQuery() { System.out.println("----testNonQuery----".html); String sql = "insert into `user` values('user_test','pwd_test'.html)"; QueryRunner query = new QueryRunner(); Connection conn = null; try { conn = getConnection(); query.update(conn, sql.html); } catch (SQLException e.html) { e.printStackTrace(); } finally { try { DbUtils.close(conn.html); } catch (SQLException e.html) { // ignore } } } }3. 关于 wrappers 包在新建 QueryRunner 时，可以通过覆盖父类的方法 wrap 来使用特定的包装器：QueryRunner query = new QueryRunner() { protected ResultSet wrap(ResultSet rs.html) { return StringTrimmedResultSet.wrap(rs.html); } };四、扩展RowProcessor 接口ResultSetHandler 接口说明： 本文基于 commons-dbutils 1.2 版本编写，属于较早期的版本。示例中使用的 MySQL 驱动类名 org.gjt.mm.mysql.Driver 已过时，新版本建议使用 com.mysql.cj.jdbc.Driver 或 com.mysql.jdbc.Driver，且现代开发中建议结合连接池使用。
引言本文翻译自 javarevisited。如需转载本文，请先参见文章末尾处的转载要求。本文收集了一些用于提升 JDBC 性能的方法。Java 应用或 JavaEE Web 应用的性能至关重要，尤其是数据库后端对整体应用性能的影响显著。你是否经历过 Java 应用响应缓慢的案例（例如处理一个简单的请求都要花费数秒时间用于数据库访问、分页或排序）？下面这些贴士或许能帮助提升 Java 应用的性能。它们非常简单，同时也适用于其他使用数据库作为后端存储的编程语言。这几个 JDBC 性能贴士虽然基础，但在实践中很多程序员经常忽略它们。当然，你也可以将本文视为数据库性能优化的一般性建议。贴士一：使用缓存统计应用中有多少数据库调用，并设法将它们减到最少。不管你信不信，性能问题的罪魁祸首大多数情况下是访问数据库的代码。因为连接到数据库需要建立连接（Connections），涉及往返的网络传输以及数据库系统后端的处理。如果你可以把数据缓存下来，这是减少数据库调用最好的方式。即使你的应用数据是完全动态的，短暂的缓存也可以节省很大的数据库往返传输开销。加速 Java 应用后，至少能减少 20-50% 的数据库调用。如果想找出数据库调用热点，仅仅把 DAO 层的每个 DB 调用记录到日志中就行；如果记录好每个线程进入和退出数据库访问的时间更好，它能告诉你一个调用究竟花了多少时间。贴士二：使用数据库索引检查数据库列（Columns）上是否有索引。如果你正做查询发现所花的时间比预想的要长，那么首先想到的是检查在列上（正在查询的 WHERE 子句中的那列）是否建立了索引。程序员常犯这个错误，有索引和没有索引在做查询时有巨大的差异。这条贴士在性能上至少能提速 100%，当然合适的索引更重要。太多的索引反而会减慢数据的插入和更新操作，因此使用索引的时候要小心。像 id、类别（category）、类（class）等字段上做索引是经常使用的场景。贴士三：使用 PreparedStatement使用 PreparedStatement 或者存储过程（Stored Procedure）执行查询时，PreparedStatement（预处理语句）比普通的 Statement 对象要快。因为数据库可以对查询语句做预处理和查询缓存计划。因此，总是使用参数化形式的预处理语句，例如：SELECT * FROM table WHERE id=?而不要使用字符串拼接的方式：SELECT * FROM table WHERE id='" + id + "'"虽然后者仍是一个预处理语句但不是参数化的，使用第二种查询方式在性能上没有任何优势。更多参考可以查看 JDBC 为什么要使用 PreparedStatement 而不是 Statement。贴士四：使用数据库连接池连接池用来存放数据库连接（Connection）。创建数据库连接是个比较慢的过程而且会耗很长时间，所以如果每个请求都要创建一个连接，那么显然响应时间将会更长。用连接池根据上游的流量及并发请求数创建适当数量的连接。即使连接池在开始的请求中创建连接、缓存连接会比较慢，但总体还是能减少不少开销。贴士五：使用 JDBC 批量更新使用 JDBC 批量更新操作能显著提升 Java 数据库应用的性能。你应该始终用批量处理来执行插入和更新操作，通过使用 Statement 或者 PreparedStatement 做批量查询，并用 executeBatch() 方法执行。贴士六：取消自动提交查询时设置 setAutoCommit(false.html)。默认 JDBC 连接自动提交模式是打开的，意味着每个独立的 SQL 语句都将在自己的事务中执行。然而你可以把 SQL 语句按组归到一个逻辑事务中去，这样通过调用 commit() 或 rollback() 要么提交要么回滚。试着运行相同数量的查询次数，对比一下使用自动提交和不使用自动提交时的性能区别有多大的差异性。总结这些 Java 数据库应用性能小提示看起来非常简单，很多高级 Java 程序员能熟练运用在生产级代码中，但我还看到很多 Java 程序员并没有在乎这些，直到他们发现 Java 应用变得很慢时。所以对于新手来说有必要记住并使用它。同时你还可以使用这些 Java 性能小提示作为代码审查机制的一个参考，在你写的 Java 应用是使用数据库作为后端存储的时候。以上就是关于如何改善 Java 应用程序性能的贴士，如果你有什么更好的提示不妨给我留言。说明：本文核心观点翻译自 2012 年的技术文章。虽然 JDBC 基础原理至今未变，上述优化策略仍然适用，但具体实现细节可能随 JDK 版本或数据库驱动更新而有所差异。