如何测定JDBC的机能
副标题#e#
Java数据库毗连(JDBC)被遍及用在Java应用措施中。在本篇文章中,我们将接头如何测定JDBC的机能,如何判定JDBC子系统中的哪一部门需要举办优化。
焦点的java.sql界面
我们的目标是提高应用措施的机能。一般环境下,我们需要对应用措施举办阐明,找出个中的瓶颈。虽然了,要对漫衍式应用措施举办有效的阐明是较量坚苦的,I/O是阐明的一个重点,这是由漫衍式应用措施的特点抉择的,漫衍式应用措施中的线程需要耗费大量的时间期待I/O操纵。今朝还不清楚线程因期待读、写操纵而阻塞是瓶颈的一部门呢照旧一个无关紧急的小问题。在举办阐明时,有一个独立的通信系统测试尺度是重要的。那么在测试JDBC子系统的机能时,我们该当测试哪些指标呢?
在java.sql软件包中,有三个接口构成了JDBC的焦点:Connection、Statement和ResultSet。与数据库的正常交互包罗下面的几部门:
·从数据库驱动措施中得到一个Connection工具。
·从Connection工具中获取可以或许执行指定的SQL语句的Statement工具
·假如SQL语句需要从数据库中读取数据,则利用Statement工具获取一个提供对数据库中的数据举办会见的ResultSet工具。
下面的例子通过会见指定命据库表的每行记录的所有域、将每行的数据存储到String []、并将所有的行放到一个向量中,演示了尺度的数据库交互进程。
public static Vector getATable(String tablename, Connection Connection)
throws SQLException
{
String sqlQuery = "SELECT * FROM " + tablename;
Statement statement = Connection.createStatement();
ResultSet resultSet = statement.executeQuery(sqlQuery);
int numColumns = resultSet.getMetaData().getColumnCount();
String[] aRow;
Vector allRows = new Vector();
while(resultSet.next())
{
aRow = new String[numColumns];
for (int i = 0; i < numColumns; i++)
file://ResultSet的会见是从1开始的,数组是从0开始的。
aRow[i] = resultSet.getString(i+1);
allRows.addElement(aRow);
}
return allRows;
}
在java.sql或其他的SDK中没有Connection、Statement和ResultSet这三个工具的详细实现,这些工具以及其他的JDBC接口都是由数据库驱动措施的厂商开拓的,并被作为数据库驱动措施的一部门包罗在驱动措施软件包中。假如要打印出Connection工具或利用的其他工具的类名,大概会看到雷同XXXConnection、XXXStatement、XXXConnectionImpl、XXXStatementImpl等字符串,个中的XXX就是正在利用的数据库的名字,譬喻Oracle。
假如我们要测试例子中getATable()要领的JDBC的机能,可以简朴地在该要领的开始处和末端处添加System.currentTimeMillis(),二者之间的时间差就是getATable()要领执行所利用的时间。只要数据库的交互进程与其他进程没有搅和在一起,就可以利用这种要领测试一个要领的JDBC机能。但凡是环境下,Java应用措施的的数据库交互进程漫衍在很多类的很多要领中,并且很难将数据库交互进程单独疏散出来。那么在这种环境下我们应该如何测试数据库交互进程的机能呢?
一个抱负的要领是在所有的JDBC类中都内置丈量机能的本领,然后可以在需要对其机能举办监测时简朴地打开监测成果就可以了。正常环境下,JDBC类没有提供这种本领,但我们可以利用具备这种成果的类来替换它们,我们替换类的方针是提供与Proxy很是相似的工具。
利用一个接口的专用封装工具封装该接口的工具是一种有多种用途的成熟技能,collection类同步的封装工具就是最著名的一个例子,但尚有其他很多用途。SDK中甚至有一个专门在运行时才生成封装工具的类:java.lang.reflect.Proxy类。封装工具也被称作署理工具,假如在本篇文章中利用署理工具这个术语,会使对封装JDBC工具的表明更巨大,因此,在本篇文章中仍然会僵持利用封装类。
要在上述成果的基本上添加测试数据库交互进程的成果,还需要对应用措施的其他部门作一些改变,很明明的是,这样作需要必然的价钱。
幸运的是,当一个框架象JDBC那样险些完全回收接口来界说时,要用别的的实现替换个中的作一个类就相当简朴了。我们可以利用一个封装类替换一个接口的任何一种实现,该封装类封装原有的类,并转发所有对本来类的要领的挪用。在本篇文章中,我们可以利用一个封装类替换掉JDBC类,将我们监测JDBC机能的成果安排在封装类中,然后使监测成果随整个应用措施的执行而执行。
#p#副标题#e#
封装Connection类
我们将首先接头Connection类的封装。下面的ConnectionWrapper类实现了Connection类,该类有一个Connection类的实例变量和利用构建器的参数初始化实例变量的构建器,大大都的Connection类的要领被简朴地界说为将挪用寄托给实例变量:
#p#分页标题#e#
package tuning.jdbc;
import java.sql.*;
import java.util.Map;
public class ConnectionWrapper implements Connection
{
protected Connection realConnection;
public Connection realConnection () {
return realConnection;
}
public ConnectionWrapper (Connection Connection) {
realConnection = Connection;
}
public void clearWarnings() throws SQLException {
realConnection.clearWarnings();
}
public void close() throws SQLException {
realConnection.close();
}
public boolean isClosed() throws SQLException {
return realConnection.isClosed();
}
public void commit() throws SQLException {
realConnection.commit();
}
...
我省略了大部门的要领,但它们都切合下面的的模板,在需要利用从数据库驱动措施中获取的Connection工具的处所,我们可以简朴地利用ConnectionWrapper封装Connection工具,而利用ConnectionWrapper工具。无论在那边获取了Connection工具,我们都需要在该处添加下面的二行代码:
Connection dbConnection = getConnectionFromDriver();
dbConnection = new ConnectionWrapper(dbConnection);
得到毗连是该应用措施中独一需要改变的部门,这要求发明所有得到一个Connection工具的挪用,并对该挪用举办编辑。然而,大大都的应用措施利用一个会合的署理类提供Connection工具,在这种环境下,在应用措施中利用ConnectionWrapper就很是简朴了。该署理类需要频繁地会见一个Connection工具池,因此在将一个Connection工具释放回Connection工具池中时,还需要作一些特另外事情,因为Connection工具首先需要被解包,譬喻:
public static void releaseConnection(Connection conn)
{
if (conn instanceof ConnectionWrapper)
conn = ( (ConnectionWrapper) conn).realConnection();
...
}
我们还没有真正地完成ConnectionWrapper类,ConnectionWrapper类中有一些要领不能简朴地寄托,这些就是提供各类Statement工具的要领:
public Statement createStatement() throws SQLException {
return new StatementWrapper(realConnection.createStatement(), this);
}
public Statement createStatement(int resultSetType,
int resultSetConcurrency) throws SQLException {
return new StatementWrapper(
realConnection.createStatement(resultSetType,
resultSetConcurrency), this);
}
public CallableStatement prepareCall(String sql) throws SQLException {
return new CallableStatementWrapper(
realConnection.prepareCall(sql), this, sql);
}
public CallableStatement prepareCall(String sql, int resultSetType,
int resultSetConcurrency) throws SQLException {
return new CallableStatementWrapper(
realConnection.prepareCall(sql, resultSetType,
resultSetConcurrency), this, sql);
}
public PreparedStatement prepareStatement(String sql)
throws SQLException {
return new PreparedStatementWrapper(
realConnection.prepareStatement(sql), this, sql);
}
public PreparedStatement prepareStatement(String sql, int resultSetType,
int resultSetConcurrency) throws SQLException {
return new PreparedStatementWrapper(
realConnection.prepareStatement(sql, resultSetType,
resultSetConcurrency), this, sql);
}
如上所示,我们需要界说三种Statement封装类,别的,我们还需要为DatabaseMetaData界说一个封装类,该封装类必需是完备的,因为DatabaseMetaData可以或许返回用来建设DatabaseMetaData的Connection工具,因此我们需要确保Connection工具是颠末封装过的,而不是我们没有封装过的Connection工具。
public DatabaseMetaData getMetaData() throws SQLException {
return new DatabaseMetaDataWrapper(
realConnection.getMetaData(), this);
}
封装statement类
Statement、PreparedStatement和CallableStatement这三个statement类的封装类相似:
public class StatementWrapper implements Statement
{
protected Statement realStatement;
protected ConnectionWrapper connectionParent;
public StatementWrapper(Statement statement, ConnectionWrapper parent)
{
realStatement = statement;
connectionParent = parent;
}
public void cancel() throws SQLException {
realStatement.cancel();
}
...
我选择了将PreparedStatementWrapper实现为StatementWrapper的一个子类,但这并不是必需的。我们可以将PreparedStatement作为Object的一个子类,实现所有的要求的要领,而不是担任Statement类的要领。
#p#分页标题#e#
public class PreparedStatementWrapper extends StatementWrapper implements PreparedStatement
{
PreparedStatement realPreparedStatement;
String sql;
public PreparedStatementWrapper(PreparedStatement statement, ConnectionWrapper parent, String sql)
{
super(statement, parent);
realPreparedStatement = statement;
this.sql = sql;
}
public void addBatch() throws SQLException {
realPreparedStatement.addBatch();
}
同样地,我选择了将CallableStatementWrapper实现为PreparedStatementWrapper的一个子类:
public class CallableStatementWrapper extends PreparedStatementWrapper implements CallableStatement
{
CallableStatement realCallableStatement;
public CallableStatementWrapper(CallableStatement statement, ConnectionWrapper parent, String sql)
{
super(statement, parent, sql);
realCallableStatement = statement;
}
public Array getArray(int i) throws SQLException {
return new SQLArrayWrapper(realCallableStatement.getArray(i), this, sql);
}
这一次,我们并没有写出全部的代码。这些Statement封装类中的一些要领不能被简朴地寄托。第一,是一个返回Connection工具的要领,我们但愿返回ConnectionWrapper工具,虽然了,这很是容易作到。下面是StatementWrapper中的要领:
public Connection getConnection() throws SQLException {
return connectionParent;
}
第二,我们有返回ResultSets的要领。这些要领需要返回ResultSet的封装类。为了担保ResultSetWrapper的一致性,我们在StatementWrapper中添加了一个通报给其构建器的lastSqlString实例变量。当我们对特定的SQL语句举办机能监测时,该实例变量就很是有用了。返回ResultsSets的要领如下所示:
//StatementWrapper要领
public ResultSet getResultSet() throws SQLException {
return new ResultSetWrapper(realStatement.getResultSet(), this, lastSql);
}
public ResultSet executeQuery(String sql) throws SQLException {
return new ResultSetWrapper(realStatement.executeQuery(sql), this, sql);
}
//PreparedStatementWrapper要领
public ResultSet executeQuery() throws SQLException {
return new ResultSetWrapper(realPreparedStatement.executeQuery(), this, sql);
}
第三,一些要领利用了java.sql.Array工具。由于这些Array工具可以或许返回ResultSet,因此我们再次需要提供一个Array封装工具,以便返回ResultSetWrapper而不是普通的ResultSets。别的,我们还需要处理惩罚Array工具被通报给setArray()要领的环境:假如通报的是Array封装工具,则在被通报给PreparedStatement前,该工具需要被解封装:
public void setArray(int i, Array x) throws SQLException {
if (x instanceof SQLArrayWrapper)
realPreparedStatement.setArray(i, ((SQLArrayWrapper) x).realArray);
else
realPreparedStatement.setArray(i, x);
}
public Array getArray(int i) throws SQLException {
return new SQLArrayWrapper(realCallableStatement.getArray(i), this, sql);
}
最后,我们建设这些封装类的目标是可以或许实现机能的监测。我们要在下面的要领中添加测试JDBCLogger类机能的成果,这样每个要领都有一个对被封装在测试挪用中的真正执行要领的挪用。我们将sql字符串和当前的线程通报给测试挪用,因为对付任何范例的测试挪用来说,这二个参数都是十分重要的,尤其是在丈量进程运行的时间时更是如此。别的,需要留意的是,我还从头界说了返回ResultSets的executeQuery()要领,以便在个中插入测试类:
//StatementWrapper要领
public void addBatch(String sql) throws SQLException {
realStatement.addBatch(sql);
lastSql = sql;
}
public boolean execute(String sql) throws SQLException {
Thread t = Thread.currentThread();
JDBCLogger.startLogSqlQuery(t, sql);
boolean b = realStatement.execute(sql);
JDBCLogger.endLogSqlQuery(t, sql);
lastSql = sql;
return b;
}
public int[] executeBatch() throws SQLException {
Thread t = Thread.currentThread();
JDBCLogger.startLogSqlQuery(t, "batch");
int[] i = realStatement.executeBatch();
JDBCLogger.endLogSqlQuery(t, "batch");
return i;
}
public ResultSet executeQuery(String sql) throws SQLException {
Thread t = Thread.currentThread();
JDBCLogger.startLogSqlQuery(t, sql);
ResultSet r = realStatement.executeQuery(sql);
JDBCLogger.endLogSqlQuery(t, sql);
lastSql = sql;
return new ResultSetWrapper(r, this, sql);
}
public int executeUpdate(String sql) throws SQLException {
Thread t = Thread.currentThread();
JDBCLogger.startLogSqlQuery(t, sql);
int i = realStatement.executeUpdate(sql);
JDBCLogger.endLogSqlQuery(t, sql);
lastSql = sql;
return i;
}
file://PreparedStatementWrapper要领
public boolean execute() throws SQLException {
Thread t = Thread.currentThread();
JDBCLogger.startLogSqlQuery(t, sql);
boolean b = realPreparedStatement.execute();
JDBCLogger.endLogSqlQuery(t, sql);
return b;
}
public ResultSet executeQuery() throws SQLException {
Thread t = Thread.currentThread();
JDBCLogger.startLogSqlQuery(t, sql);
ResultSet r = realPreparedStatement.executeQuery();
JDBCLogger.endLogSqlQuery(t, sql);
return new ResultSetWrapper(r, this, sql);
}
public int executeUpdate() throws SQLException {
Thread t = Thread.currentThread();
JDBCLogger.startLogSqlQuery(t, sql);
int i = realPreparedStatement.executeUpdate();
JDBCLogger.endLogSqlQuery(t, sql);
return i;
}
封装ResultSet类
ResultSetWrapper类也主要包罗寄托要领:
#p#分页标题#e#
public class ResultSetWrapper implements ResultSet
{
ResultSet realResultSet;
StatementWrapper parentStatement;
String sql;
public ResultSetWrapper(ResultSet resultSet, StatementWrapper statement, String sql) {
realResultSet = resultSet;
parentStatement = statement;
this.sql = sql;
}
public boolean absolute(int row) throws SQLException {
return realResultSet.absolute(row);
}
...
个中也有一些要领不是简朴的寄托要领,getStatement()要领返回生成ResultSet的statement工具,我们需要让它返回StatementWrapper工具:
public Statement getStatement() throws SQLException {
return parentStatement;
}
The getArray() methods need to return a wrapped Array object:
public Array getArray(int i) throws SQLException {
return new SQLArrayWrapper(realResultSet.getArray(i), parentStatement, sql);
}
public Array getArray(String colName) throws SQLException {
return new SQLArrayWrapper(realResultSet.getArray(colName), parentStatement, sql);
}
最后,我们需要添加测试进程。很多开拓人员都错误地认为,差异的Statement.execute*()要领城市引起数据库交互进程带来的承担,对付数据库的更新和读取少量的数据库记录而言,这是正确的。假如读取的数据库记录的量较大,ResultSet.next()需要大量的时间从数据库中读取记录。假如读取的记录太多,ResultSet.next()挪用所需要的时间就会多于SQL语句执行的时间。因此,测试ResultSet.next()挪用的时间也就是理所虽然的了。
public boolean next() throws SQLException {
Thread t = Thread.currentThread();
JDBCLogger.startLogSqlNext(t, sql);
boolean b = realResultSet.next();
JDBCLogger.endLogSqlNext(t, sql);
return b;
}
假如需要,尚有一些ResultSet挪用可以丈量,譬喻previous()、insertRow()等,但大大都的应用措施只需要对next()举办丈量。
JDBC封装类架构
上面接头了需要封装的类,我没有明晰地说明Array和DatabaseMetaData的封装类,但它们都较量简朴,只需要返回ResultSetWrappers和ConnectionWrappers而不是ResultSets和Connections类。利用封装工具测试数据库交互进程机能的技能合用于JDBC 1、JDBC 2和将来的JDBC 3,它们在接口界说方面互不沟通(因此需要差异的封装类。但我们可以用同一种方法建设所有差异版本下的封装类。
我没有接头的是JDBCLogger,该类的一个简朴的实现中不挪用测试要领,但将不提供测试成果:
package tuning.jdbc;
public class JDBCLogger
{
public static void startLogSqlQuery(Thread t, String sql) {}
public static void endLogSqlQuery(Thread t, String sql) {}
public static void startLogSqlNext(Thread t, String sql) {}
public static void endLogSqlNext(Thread t, String sql) {}
}
一个更有用的界说是测试查询的时间。下面的要领记录查询开始时的时间,并在查询竣事时得出利用的时间。由于假定在同一个线程中SQL查询不能递归(一般环境下都是这样的),下面的要领是相当简朴的:
private static Hashtable QueryTime = new Hashtable();
public static void startLogSqlQuery(Thread t, String sql)
{
if (QueryTime.get(t) != null)
System.out.println("WARNING: overwriting sql query log time for " + sql);
QueryTime.put(t, new Long(System.currentTimeMillis()));
}
public static void endLogSqlQuery(Thread t, String sql)
{
long time = System.currentTimeMillis();
time -= ((Long) QueryTime.get(t)).longValue();
System.out.println("Time: " + time + " millis for SQL query " + sql);
QueryTime.remove(t);
}
利用JDBCLogger类中的这些要领的输出将如下所示:
Time: 53 millis for SQL query SELECT * FROM JACKTABL
#p#分页标题#e#
对付每次查询执行来说,这将使我们可以或许准确地测试SQL查询所利用的时间,也可以或许计较出JDBCLogger类中所有查询所需要的时间。我常常测试的是最小、最大、平均、平均毛病等值,这些值在测试大局限的系统的机能时更有用。
利用JDBC封装类框架
我们已经先容了很是有用的在应用措施的开拓和部署阶段测试JDBC挪用机能的要领。由于封装类较量简朴,并且成果强大,又不需要对应用措施举办大量的修改,它们可以被保存在已经部署好的应用措施中,建设一个可设置的JDBCLogger类将使我们可以或许按照本身的需要开启或封锁测试成果。
在开拓阶段,由于可以或许计较出累积的时间价钱,我们可以或许操作这些类分辨出个此外需要较大时间价钱的数据库交互进程和反复的数据库交互进程,哪个的时间价钱更大。分辨出时间价钱较大的数据库交互进程是我们改造应用措施机能的第一步。在开拓阶段,这些封装类可以用来发明应用措施的理论机能和实际机能之间的差距,有助于我们阐明为什么会有差距。
在操作这些类找出JDBC的机能瓶颈在那边后,我们就可以对数据库的接口举办调解了。我将在今后的文章中继承接头JDBC机能的技能。