Java 数据库连接 (JDBC
) 是一个功能强大的 API,它弥补了 Java 应用程序与关系数据库之间的差距。通过利用 JDBC,您可以无缝地与数据库交互以存储、检索和操作数据。但是,要有效使用 JDBC,需要遵循最佳实践,以确保代码的最佳性能、安全性和可维护性。
正如我之前提到的观点,学习一个新事物,首先要掌握其最佳实践,下面让我们来研究一下 JDBC
最佳实践的内容。
JDBC简介
我们开发的同一套Java代码是无法操作不同的关系型数据库,因为每一个关系型数据库的底层实现细节都不一样。如果这样,问题就很大了,在公司中可以在开发阶段使用的是MySQL数据库,而上线时公司最终选用oracle数据库,我们就需要对代码进行大批量修改,这显然并不是我们想看到的。我们要做到的是同一套Java代码操作不同的关系型数据库,而此时sun公司就指定了一套标准接口(JDBC),JDBC中定义了所有操作关系型数据库的规则。众所周知接口是无法直接使用的,我们需要使用接口的实现类,而这套实现类(称之为:驱动)就由各自的数据库厂商给出。
JDBC本质
官方(sun公司)定义的一套操作所有关系型数据库的规则,即接口
各个数据库厂商去实现这套接口,提供数据库驱动jar包
我们可以使用这套接口(JDBC)编程,真正执行的代码是驱动jar包中的实现类
JDBC好处
各数据库厂商使用相同的接口,Java代码不需要针对不同数据库分别开发
可随时替换底层数据库,访问数据库的Java代码基本不变
以后编写操作数据库的代码只需要面向JDBC(接口),操作哪儿个关系型数据库就需要导入该数据库的驱动包,如需要操作MySQL数据库,就需要再项目中导入MySQL数据库的驱动包。
在与数据库进行交互之前,Java数据库连接(JDBC)技术依赖于一个关键组件——JDBC驱动程序。这个驱动程序是特定于每种数据库系统的软件库,例如MySQL、Oracle或PostgreSQL。它的作用是将Java应用程序中的代码转换为数据库能够识别和执行的指令。
驱动程序
在软件开发中,选择和查找合适的数据库驱动程序是至关重要的一步。以下是一些查找数据库驱动程序的方法:
-
官方文档和网站:大多数数据库系统(如MySQL、PostgreSQL、MongoDB等)都有自己的官方网站和文档。官方文档通常提供了详细的安装指南、配置说明以及驱动程序下载链接。通过官方文档查找数据库驱动程序是最可靠的途径。
-
包管理工具:对于使用现代编程语言的开发者来说,包管理工具是查找和安装数据库驱动程序的便捷途径。例如,Java的Maven和Gradle,JavaScript的npm和yarn,Python的pip,Go的go mod等,这些工具都有丰富的库和驱动程序供选择。
-
第三方库和框架:一些第三方库和框架(如Hibernate、Spring Data、Django ORM等)通常内置或推荐使用特定的数据库驱动程序。使用这些库和框架时,可以直接参考其文档,找到合适的驱动程序。
通过以上方法,可以有效地找到并选择适合项目需求的数据库驱动程序。确保驱动程序的版本与数据库和应用程序的版本兼容,以避免潜在的兼容性问题和性能问题。
连接 URL
有了驱动程序后,就该告诉它在哪里找到数据库了。此信息被打包成一个称为连接 URL的特殊字符串。这就像给聚会发指示:
-
数据库类型: 这告诉驱动程序使用哪个解释器(例如,
jdbc:mysql
MySQL)。 -
主机: 数据库服务器的地址(通常是计算机名称或 IP 地址)。
-
端口: 数据库监听的特定端口。
-
数据库名称: 要连接的特定数据库的名称。
-
可选附加功能: 可以添加用户名和密码以确保安全或数据库其他设置。
以下是 MySQL
数据库的连接 URL
示例:
jdbc:mysql://localhost:3306/mydatabase?user=fred&password=secret
在此示例中:
-
jdbc:mysql
:告诉驱动程序我们正在使用 MySQL。 -
localhost
:数据库服务器与我们的 Java 应用程序位于同一台机器上。 -
3306
:MySQL 的默认端口。 -
mydatabase
:我们想要连接的特定数据库。 -
user=fred&password=secret
:访问数据库的登录凭据(出于安全原因,这些凭据通常是隐藏的)。
安全、高效使用 JDBC
Java 应用程序中通过电子邮件地址搜索用户。虽然该 Statement
对象似乎是一种快速解决方案,但它可能会带来安全风险和性能瓶颈。
-
语句和 SQL 注入:
String email = userInput;
String sql = "SELECT * FROM users WHERE email = '" + email + "'";
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql);
此代码段使用Statement
容易受到 SQL 注入攻击。恶意用户可以输入类似这样的电子邮件,user@example.com' OR '1'='1'
从而绕过预期的检查并可能检索所有用户数据。
-
性能
虽然 Statement
看起来比较简单,但对于具有不同值的重复查询,其性能可能会较低。即使结构保持不变,数据库每次都需要重新编译整个查询。
PreparedStatement
对象提供了一个强大且安全的替代方案:
-
分离查询和数据:您可以使用占位符()定义模板查询,
?
以供动态用户输入。
String sql = "SELECT * FROM users WHERE email = ?";
-
稍后绑定变量:执行查询时,将实际值(如电子邮件地址)绑定到这些占位符。
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, email);
ResultSet resultSet = preparedStatement.executeQuery();
PreparedStatement的好处:
PreparedStatement相比普通的Statement有几个主要优点:
-
性能更好:PreparedStatement会预编译SQL语句,只编译一次就可以多次执行,而普通Statement每次执行都要编译。对于需要重复执行的SQL,PreparedStatement的性能明显更好。
-
防止SQL注入:PreparedStatement使用参数化查询,可以有效防止SQL注入攻击。它会自动转义特殊字符,避免恶意SQL被注入。
-
更易于维护:由于使用占位符,SQL语句和参数是分离的,代码更清晰易读,也更容易修改维护。
-
支持批量处理:PreparedStatement可以通过addBatch()方法一次性发送多条SQL语句,提高批量处理的效率。
-
更好的类型处理:PreparedStatement可以为参数设置具体的数据类型,避免类型转换错误。
-
更好的可读性:使用参数化查询使SQL语句结构更清晰,提高了代码的可读性。
-
缓存机制:数据库可能会缓存PreparedStatement的执行计划,进一步提升性能。
这些优点使PreparedStatement
成为执行SQL的首选方式,特别是对于需要重复执行或包含用户输入的SQL
语句。
ResultSet
假设我们执行了一个从数据库检索数据的查询。结果存储在一个特殊对象中:ResultSet
。要访问此数据,您需要一次迭代(循环)一行:ResultSet
。
工作原理如下:
-
检查结果:使用执行查询后
PreparedStatement
,使用executeQuery
方法获取一个ResultSet
对象。此对象保存检索到的数据。 -
循环遍历行:使用
while
循环遍历ResultSet
。next
方法将ResultSet
光标移动到下一行数据。只要还有更多行(next
返回true
),循环就会继续。 -
访问数据:在循环内部,根据数据库列中存储的数据类型使用适当的
getter
方法来访问当前行中的特定值。以下是一些常见的 getter 方法:-
getString(int columnIndex)
:从列中检索字符串值。 -
getInt(int columnIndex)
:从列中检索整数值。 -
getDouble(int columnIndex)
:从列中检索双精度浮点值。 -
对于其他数据类型(如日期、布尔值等)也有类似的方法。
-
下面是一个示例循环,它迭代ResultSet
并打印每行的电子邮件和年龄:
while (resultSet.next()) {String email = resultSet.getString("email");int age = resultSet.getInt("age");System.out.println("Email: " + email + ", Age: " + age);
}
正确关闭资源
如果ResultSet
从图书馆借来的一本书。阅读完毕(访问数据)后,使用 close
方法关闭它至关重要。这会释放对象所持有的资源ResultSet
,让数据库能够有效地处理它们。
这是一个很好的做法:
try (ResultSet resultSet = preparedStatement.executeQuery()) {
// 数据遍历和处理
} catch (SQLException e) {
// 异常处理
}
数据类型
从数据库检索数据时,使用 ResultSet
的正确 getter
方法非常重要。根据数据库列中存储的数据类型选择合适的方法。例如,对于存储字符串的 email
列,应该使用 getString
方法来检索值。使用错误的方法(例如对电子邮件字符串使用 getInt
)可能会导致意外结果甚至异常。
起源于两个问题:
-
当一个
ResulSet
被执行方法返回,如果不使用close()
方法,会怎么样? -
Statement支持不支持并发调用?
ResulSet资源释放
在 close()
方法注释中,我们得到该方法是为了释放ResulSet对象占用的各种资源。在 Java 中,ResultSet
是用于表示 SQL 查询结果的对象。ResultSet
对象维护了指向查询结果的光标,可以让你逐行访问查询返回的数据。ResultSet
的 close()
方法用于关闭该 ResultSet
对象,释放资源并释放与数据库的连接。一旦调用了 close()
方法,该 ResultSet
对象将不再可用,并且不能再使用它来访问查询结果或提取数据。当你完成对 ResultSet
对象的操作后,应该及时调用 close()
方法来释放资源,尤其是当你不再需要访问查询结果或当你需要释放数据库连接时。这可以帮助释放数据库资源、减少内存占用,并允许数据库服务器回收相关资源以供其他请求使用,从而提高系统性能和资源利用率。
但是我在实际使用当中,并没有显式调用过 close()
也从来没发生数据库连接超限导致的异常,这一点让我非常奇怪。
首先我们看一下 close()
的具体内容:
public void close() throws SQLException { try { this.realClose(true); } catch (CJException var2) { throw SQLExceptionsMapping.translateException(var2, this.getExceptionInterceptor()); }
}
我们再看 realClose()
方法,内容太多了,我摘抄了部分内容:
第一部分:
JdbcConnection locallyScopedConn = this.connection;if (locallyScopedConn != null) {synchronized(locallyScopedConn.getConnectionMutex()) {
第二部分:
this.rowData = null;
this.columnDefinition = null;
this.eventSink = null;
this.warningChain = null;
this.owningStatement = null;
this.db = null;
this.serverInfo = null;
this.thisRow = null;
this.fastDefaultCal = null;
this.fastClientCal = null;
this.connection = null;
this.session = null;
this.isClosed = true;
第一部分显式获取了当前连接的互斥锁,然后进行一系列操作,说明改部分操作对于一个 java.sql.Connection
使用互斥锁操作是线程安全,也就是串行的。
第二部分是关闭之后对于类成员属性的一些重置。其中看到倒数第三行 this.connection = null;
就是释放当前连接引用,请注意这并不是把连接资源释放了,不同于 Connection
的 close()
方法。
然后我们在 com.mysql.cj.jdbc.StatementImpl
类中找到了对应的调用:
protected void closeAllOpenResults() throws SQLException { JdbcConnection locallyScopedConn = this.connection; if (locallyScopedConn != null) { synchronized(locallyScopedConn.getConnectionMutex()) { if (this.openResults != null) { Iterator var3 = this.openResults.iterator(); while(var3.hasNext()) { ResultSetInternalMethods element = (ResultSetInternalMethods)var3.next(); try { element.realClose(false); } catch (SQLException var7) { AssertionFailedException.shouldNotHappen(var7); } } this.openResults.clear(); } } }
}
然后我们找到了 com.mysql.cj.jdbc.StatementImpl#implicitlyCloseAllOpenResults
方法,最终找到了其中一个入口方法 com.mysql.cj.jdbc.StatementImpl#executeQuery
,源码部分如下:
public ResultSet executeQuery(String sql) throws SQLException {try {synchronized(this.checkClosed().getConnectionMutex()) {JdbcConnection locallyScopedConn = this.connection;this.retrieveGeneratedKeys = false;this.checkNullOrEmptyQuery(sql);this.resetCancelledState();this.implicitlyCloseAllOpenResults();
也就是说每一次执行MySQL操作,都会将所有打开的 ResultSet
对象都关闭掉。
所以对于 ResultSet
对象来说,下一次调用都会关闭,即使不手动关闭释放资源也是可以接受的。
Statement并发
虽然 Statement
官方资料中并没有明显说是否支持并发,但我一直认为是不支持并发的,忘记知识的来源了,再去搜索的话,也得到了很多印证。
但是对于一个对象来说,无法禁止并发调用,假如用户自己并发调用了,会怎么样呢?
我写了个Demo测试了一下,内容如下:
def connection = SqlBase.getConnection("jdbc:mysql://127.0.0.1:3306/funtester", "root", "funtester")def statement = SqlBase.getStatement(connection)def test = {def query = statement.executeQuery("select * from user")while (query.next()) {println query.getString("name")println query.getString("id")}query.close()}10.times {Thread.startVirtualThread {test()}}sleep(1.0)
代码Groovy写的,用上了JDK 21最新的虚拟线程功能,感觉良好,最后加了一行 sleep(1.0)
因为虚拟线程并不会阻塞 JVM
关闭,这一点跟 Golang
的协程 goroutine
一样。
结果就发现了报错:
Exception in thread "" java.sql.SQLException: Operation not allowed after ResultSet closed
我们根据报错信息找到了 com.mysql.cj.jdbc.result.ResultSetImpl#checkClosed
方法,内容如下:
protected final JdbcConnection checkClosed() throws SQLException { JdbcConnection c = this.connection; if (c == null) { throw SQLError.createSQLException(Messages.getString("ResultSet.Operation_not_allowed_after_ResultSet_closed_144"), "S1000", this.getExceptionInterceptor()); } else { return c; }
}
这个 connection
表示的就是与当前对象关联的 JdbcConnection
,但是在问题1中 close()
方法第二部分代码分享,当调用 close()
方法时会将对象的 connection
属性变成 null
。所以就会报异常了。
阅读源码的好处
阅读源代码对工作和个人成长有着广泛而深远的影响。代码是软件工程的核心,阅读源代码不仅是对代码功能的理解,更是对整个软件生态系统的深入探索。当我们深入代码之中,我们不仅仅了解代码是如何工作的,还能感受到代码的背后所蕴含的设计思想、优化策略、团队合作与协作等方面的价值。
首先,阅读源代码能够帮助我们更全面、更深入地理解项目的架构和设计。透过代码,我们能够窥见不同模块、组件之间的交互方式,理解数据流、逻辑和功能实现的关系。通过对代码的解读,我们能够建立起对项目整体结构和工作方式的更深入认识,这对于项目的维护和开发至关重要。
其次,阅读源代码也是一个学习和成长的过程。我们可以从其他人的代码中学习到不同的编码技巧、最佳实践、设计模式和解决问题的方法。这种学习方式让我们接触到各种领域和风格的代码,提高了我们的编程能力和解决问题的能力。
另外,阅读代码也为我们提供了一个优秀的调试和问题解决的平台。通过理解代码的工作原理,当出现问题时能更快地定位和解决。我们能够更准确地判断问题的根源,并采取相应的措施来修复代码中的错误或提升代码的性能。
此外,阅读源代码有助于促进团队协作和沟通。理解其他人的工作方式和风格有助于更好地与团队成员合作,减少代码冲突和理解偏差。更好地理解彼此的工作和贡献,有助于形成更加和谐高效的团队。
总的来说,阅读源代码是一种不断学习、提高编程技能、加深对项目理解的过程。虽然这需要时间和耐心,但它对于个人和团队的成长和发展都有着积极的影响。
高效的资源管理
想象一下,您的 Java 应用程序与数据库交互就像访问图书馆一样。您需要借用连接来访问数据,就像借书阅读一样。但是,就像读完书后归还书一样,及时关闭连接对于高效的数据库交互至关重要。
关闭连接
-
为什么要关闭连接?
数据库连接是一种宝贵的资源。不必要地保持它们打开可能会导致:
-
资源耗尽:如果您的数据库保持打开状态,则数据库可能会耗尽其他用户的可用连接。
-
性能下降:打开的连接会消耗数据库服务器上的资源,影响整体性能。
-
结束技巧:
try-with-resources
,这是首选方法。即使发生异常,它也会在代码块末尾自动关闭连接(以及其他资源,如 ResultSet)。
try (Connection connection = DriverManager.getConnection(DB_URL, USER, PASSWORD)) {// 使用连接资源
} catch (SQLException e) {// 处理异常
}
当然你也可以使用 finally
代码块来手动完成这些工作。
Connection connection = null;
try {connection = DriverManager.getConnection(DB_URL, USER, PASSWORD);// 使用连接资源
} catch (SQLException e) {// 处理异常
} finally {if (connection != null) {connection.close();}
}
事务
MySQL 中的事务是一种重要的数据库功能,用于管理多个SQL操作作为一个不可分割的单元。事务确保了数据的完整性和一致性,通常使用以下几个关键字来控制事务操作:
-
BEGIN 或 START TRANSACTION: 标志事务的开始。
-
COMMIT: 提交事务,将所有已执行的操作永久保存到数据库。
-
ROLLBACK: 回滚事务,取消所有已执行的操作,回到事务开始前的状态。
在使用事务时,可以将一系列的 SQL 操作组合在一起,确保它们要么全部成功执行并提交,要么全部失败并回滚,以维护数据的完整性。这在处理复杂的数据库操作或需要原子性的数据更新时特别有用。
连接池
连接池维护一个预先建立的数据库连接池。当应用程序需要连接时,它会从池中借用一个连接,而不是从头开始创建一个新连接。与每次创建连接相比,这可以节省时间和资源。使用 MySQL 连接池有以下几个好处:
-
性能优化: 连接池在应用启动时预先创建了一定数量的数据库连接,并管理这些连接的复用和释放。这样可以避免频繁地创建和销毁连接,从而减少了数据库的负担和响应时间,提高了应用的性能。
-
资源管理: 连接池能够限制同时打开的连接数量,防止应用程序过载数据库服务器。它还可以对连接进行有效的管理,如超时检测、空闲连接的回收等,确保数据库资源得到有效利用。
-
并发处理: 连接池允许多个线程并发地从池中获取连接,执行数据库操作,并在完成后释放连接。这种并发处理能力提高了应用程序的吞吐量和响应速度。
-
连接的复用: 连接池可以重复使用已经建立的连接,避免了频繁地重新建立连接的开销,提高了数据库操作的效率和稳定性。
-
连接的配置和监控: 连接池通常提供了配置参数和监控功能,可以对连接池的行为进行调整和监视,如最大连接数、最小空闲连接数、连接超时等,有助于优化数据库访问的管理和性能调优。
通过使用连接池,可以有效地管理数据库连接,提升应用的性能、稳定性和可扩展性,是开发和部署高效数据库应用的重要手段之一。
错误和异常
与数据库交互的道路很少是一帆风顺的。当出现问题时,会抛出异常来表示潜在问题。在 JDBC
领域,比如 SQLException
是我们的主要敌人。
处理 SQLException
假设 Java
应用程序与数据库交互,但发生了错误(例如查询中的拼写错误或网络问题)。如果不处理 SQLException
抛出的错误,可能导致程序异常,或者进程退出。正确识别处理 SQLException
能带来下面好处:
-
更好的错误诊断:通过正确识别 SQLException,可以准确定位数据库操作中的具体问题,如连接失败、语法错误或约束违反等。
-
增强应用程序稳定性:适当处理 SQLException 可以防止未处理的异常导致应用程序崩溃,提高系统的稳定性和可靠性。
-
改善用户体验:可以根据不同类型的 SQLException 提供更有意义的错误消息给用户,而不是显示通用的数据库错误。
-
支持更好的异常恢复:对不同类型的 SQLException 进行分类处理,可以实现更精细的异常恢复策略,如自动重试或回滚事务。
常见SQLException
以下是SQLExceptions
您可能会遇到的一些常见问题以及处理策略:
-
SQLSyntaxErrorException:这表示您的 SQL 查询中存在语法错误。请仔细检查您的查询是否存在拼写错误、缺少分号或语法错误。
-
SQLNonTransientException:这表示非瞬时错误,这意味着不太可能通过立即重试操作来解决。这可能是数据库访问问题、未找到表或权限错误。分析特定的错误消息并采取适当的措施,例如修复查询或检查权限。
-
SQLTransientException:这表示暂时性错误,这意味着可以通过重试操作来解决。示例包括网络问题、超时或数据库过载。您可以在再次尝试操作之前以合理的延迟实现重试逻辑。
-
数据截断:当您尝试将数据插入超出其定义大小限制的列时,会发生这种情况。检查您的数据并进行调整以适应列的限制。
记录异常
虽然捕获和处理异常至关重要,但记录错误为调试和监控提供了宝贵的工具。以下是记录重要性的原因:
-
详细信息:日志可以捕获比错误消息更详细的信息,例如时间、涉及的用户和导致错误的特定查询。
-
故障排除:日志对于解决开发过程中可能不会立即显现的问题至关重要。
-
监控:日志可以帮助您监控应用程序和数据库交互的整体运行状况,在潜在问题造成重大中断之前发现它们。
在处理 SQLException
时,必须要考虑数据安全性的问题,而且要放在首要的位置。
工具类分享JdbcUtil
package com.zxx.study.base.db;import com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException;
import com.zxx.study.base.util.ZhouxxTool;
import lombok.extern.slf4j.Slf4j;import java.sql.*;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;/*** @author zhouxx* @create 2020-04-21 20:18*/
@Slf4j
public class JdbcUtil {/*** 查询返回String二维数组* @param sql* @param args* @return*/public static String[][] queryForArray(Connection conn,String sql, Object... args) {PreparedStatement stmt = null;ResultSet rs = null;String[][] result = null;try {stmt = conn.prepareStatement(sql,ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_READ_ONLY);if (args != null) {for (int i = 0; i < args.length; i++) {stmt.setObject(i + 1, args[i]);}}
// log.info("连接信息:"+ZhouxxTool.getTimeAndThread(conn)+" [sql]="+sql);rs = stmt.executeQuery();log.info(stmt.toString());rs.last();int rows = rs.getRow();rs.beforeFirst();ResultSetMetaData rsmd = rs.getMetaData();int cols = rsmd.getColumnCount();result = new String[rows+1][cols];for (int i = 0; i < cols; i++) {result[0][i] = rsmd.getColumnName(i + 1);}int currentRow = 1;while (rs.next()) {for (int i = 0; i < cols; i++) {result[currentRow][i] = rs.getObject(i + 1).toString();}currentRow++;}} catch (SQLException e) {
// e.printStackTrace();log.error(ZhouxxTool.getTimeAndThread(e));} finally {try {if (rs != null) {rs.close();}if (stmt != null) {stmt.close();}} catch (SQLException e) {e.printStackTrace();}}return result;}/*** 查询返回List类型* @param sql* @param args* @return 多条记录的List集合*/public static List<Map<String, Object>> queryForList(Connection conn,String sql, Object... args) {PreparedStatement stmt = null;ResultSet rs = null;List<Map<String, Object>> result = new ArrayList<Map<String, Object>>();try {stmt = conn.prepareStatement(sql);if (args != null) {for (int i = 0; i < args.length; i++) {stmt.setObject(i + 1, args[i]);}}
// log.info("连接信息:"+ZhouxxTool.getTimeAndThread(conn)+" [sql]="+sql);rs = stmt.executeQuery();log.info(stmt.toString());ResultSetMetaData rsmd = rs.getMetaData();int cols = rsmd.getColumnCount();String[] colNames = new String[cols];for (int i = 0; i < cols; i++) {colNames[i] = rsmd.getColumnName(i + 1);}while (rs.next()) {Map<String, Object> row = new LinkedHashMap<String, Object>();for (int i = 0; i < cols; i++) {row.put(colNames[i], rs.getObject(i + 1));}result.add(row);}} catch (SQLException e) {
// e.printStackTrace();log.error(ZhouxxTool.getTimeAndThread(e));} finally {try {if (rs != null) {rs.close();}if (stmt != null) {stmt.close();}} catch (SQLException e) {e.printStackTrace();}}return result;}/*** 查询返回Map类型 当确定返回结果唯一时调用* @param sql* @param args* @return 一条记录的Map*/public static Map<String, Object> queryForMap(Connection conn,String sql, Object... args) {List<Map<String, Object>> result = queryForList(conn,sql, args);Map<String, Object> row = null;if (result.size() == 1) {row = result.get(0);return row;} else {return null;}}/*** 执行除查询外(增加/删除/修改)* @param sql* @param args* @return 是否执行成功*/public static boolean execute(Connection conn, String sql, Object... args) {PreparedStatement stmt = null;boolean result = true;try {stmt = conn.prepareStatement(sql);if (args != null) {for (int i = 0; i < args.length; i++) {stmt.setObject(i + 1, args[i]);}}
// log.info("连接信息:"+ZhouxxTool.getTimeAndThread(conn)+" [sql]="+sql);stmt.executeUpdate();log.info(stmt.toString());}catch (MySQLTransactionRollbackException e1){log.error(ZhouxxTool.getTimeAndThread(e1));result = false;} catch (SQLException e) {
// e.printStackTrace();log.error(ZhouxxTool.getTimeAndThread(e));result = false;}finally {try {if (stmt != null) {stmt.close();}} catch (SQLException e) {e.printStackTrace();}}return result;}
}