本文参考:
- 装饰器模式和代理模式从实现方式的角度看是完全一样的, 主要以使用目的区分。 具体可以参见相关博文装饰器模式( Decorator Pattern ): 代理模式的双胞胎兄弟
设计模式用前须知
- 设计模式中一句出现频率非常高的话是,“ 在不改动。。。。的情况下, 实现。。。。的扩展“ 。
- 对于设计模式的学习者来说,充分思考这句话其实非常重要, 因为这句往往只对框架/ 工具包的设计才有真正的意义。因为框架和工具包存在的意义,就是为了让其他的程序员予以利用, 进行功能的扩展,而这种功能的扩展必须以不需要改动框架和工具包中代码为前提
- 对于应用程序的编写者, 从理论上来说, 所有的应用层级代码至少都是处于可编辑范围内的, 如果不细加考量, 就盲目使用较为复杂的设计模式, 反而会得不偿失, 毕竟灵活性的获得, 也是有代价的。
PrepareStatement 的好处
有一定java数据库开发经验的人都知道PreparedStatement 相对于Statement的优点 :
- 因为预解析(Parse),编译(Compile)了SQL语句, 计划(Plan)了SQL的数据获取路径, 所以通常来说PreparedStatement都会比Statement快, 至少不会比Statement慢
- 防止了SQL 注入, 因为它对可执行SQL和数据进行了分离 , 数据参数会被在SQL语句转化为执行计划以后, 另作单独的数据包传输过去,所以避免了拼接可能引发的SQL注入问题
- 简化了设置非标准类型参数的方法,例如 Date, Time, Timestamp, BigDecimal, InputStream (Blob) , Reader (Clob)
- 例如
preparedStatement = connection.prepareStatement("INSERT INTO Person (name, email, birthdate, photo) VALUES (?, ?, ?, ?)");
preparedStatement.setString(1, person.getName());
preparedStatement.setString(2, person.getEmail());
preparedStatement.setTimestamp(3, new Timestamp(person.getBirthdate().getTime()));
preparedStatement.executeUpdate();
如何打印PrepareStatement
但是使用PreparedStatement 对于需要保留sql 语句执行记录的场景可能会遇到问题( 例如本人所在的公司交易系统应用对所有的sql 语句都会记log ), 如果想打印以上的PreparedStatement 真正被执行的sql 语句,获得了如下形式的内容
例如 INSERT INTO Person (name, email, birthdate) VALUES (John, john@hotmail.com,19660606)
则有可能遇到问题。 因为JDBC API 并没有定义专门用于获取PreparedStatement 执行语句内容的方法, 而PreparedStatemet.toString() 获得的结果取决于使用JDBC Driver, 如果使用的是PostgreSQL 8.x and MySQL 5.x, 那直接在参数被set 后,调用 System.out.println(preparedStatement);
即可获得想要sql 语句内容。 如果是其他的JDBC Driver 例如 Oracle , 执行该语句只会获得一串对象码。
所以问题情境是,当前开发的系统中,使用了PreparedStatement 的地方都没有办法打印带参数的sql 语句, 只能打印出带问号的部分, 在应用测试阶段不便于问题的排查, 所以需要一种最低侵入式, 改动范围最小的方法支持PreparedStatement 语句的打印 。
- 可能的解决思路:
- 继承 oracle jdbc 对 PreparedStatement实现类, 重写set 方法, 在set被调用时记录参数内容, 然后自行添加toString 方法, 拼接处整的sql 语句内容
- 事实证明不可行, 因为ojdbc driver 的 PreparedStatement 的实现类的访问权限是包内权限, 所以无法继承。
- 使用Log4jdbc 或 P6Spy 等来自StackOverFlow 推荐的日志工具
- 缺点: 为了一个小需求给项目添加了新的依赖, 还需要一系列设置, 太麻烦。
- 利用设计模式之装饰器模式(Decorator), 实现一个DebuggableStatement接口, 以下列方式打印PreparedStatement
- 缺点: 除需要向原有的项目中添加几个类以外, 几乎没有。
- 继承 oracle jdbc 对 PreparedStatement实现类, 重写set 方法, 在set被调用时记录参数内容, 然后自行添加toString 方法, 拼接处整的sql 语句内容
Connection con = DriverManager.getConnection(url);
// 当DebugLevel 为OFF时, StatementFactory.getStatement(con,sql,debug);返回的依旧是PreparedStatement
DebugLevel debug = DebugLevel.ON;
String sql = "SELECT name,rate FROM EmployeeTable WHERE rate > ?";
//下面通过一个工厂类而不是Connection来获取PreparedStatement,
//PreparedStatement ps = con.prepareStatement(sql);
PreparedStatement ps = StatementFactory.getStatement(con,sql,debug);
ps.setInt(1,25);
//如果 ps 是一个实现了DebuggableStatement的对象, 便可以打印出实际执行的sql语句
//otherwise, an object identifier is displayed
System.out.println(" debuggable statement= " + ps.toString());
-
优点:
- 对原有业务部分的代码改动很小, 只需要修改一行获取PreparedStatement 的方式 。
- 不需要添加额外的依赖库
- 可以通过一个变量开关决定使用的PreparedStatement 类型, 方便随时关闭DebuggableStatement 的开销
-
代码实现方式(代码来自参考文章一, 但是做了一些改动, 避免报错无法运行):
- 需要向项目中新增以下类:
- DebuggableStatement
- StatementFactory
- SqlFormatter
- DefaultSqlFormatter
- OracleSqlFormatter
- DebugLevel
- 需要向项目中新增以下类:
-
代码说明:
- DebuggableStatement 是这里的核心, 也是应用代理模式的地方
- 需要注意的是PreparedStatement 是一个接口, 由数据库提供商的Driver 提供商提供实现, 所以这里DebuggableStatement要实现PreparedStatement 接口, 需要实现一大堆方法, 很显然,这个让我们自己实现几乎不可行, 所以这里便是应用装饰器模式的绝佳场景 。 下面抽取DebuggableStatement 的部分代码进行说明。
- DebuggableStatement 是这里的核心, 也是应用代理模式的地方
class DebuggableStatement implements PreparedStatement{
private PreparedStatement ps; //preparedStatement being proxied for.
private String sql; //original statement going to database.
private String filteredSql; //statement filtered for rogue '?' that are not bind variables.
private DebugObject[] variables; //array of bind variables
private SqlFormatter formatter; //format for dates
private long startTime; //time that statement began execution
private long executeTime; //time elapsed while executing statement
private DebugLevel debugLevel; //level of debug
....
@Override
public boolean getMoreResults(int current) throws SQLException {
return ps.getMoreResults();
....
}
注意到DebuggableStatement第一个成员对象ps 是PreparedStatement , 而我们实现PreparedStatement 的方法内容都无须自行编写, 都是直接调用ps对象的对应方法。 而ps 对象的获取是在如下的构造方法中, 通过connection 获取。
protected DebuggableStatement(Connection con, String sqlStatement, SqlFormatter formatter, DebugLevel debugLevel) throws SQLException{
//set values for member variables
if (con == null)
throw new SQLException("Connection object is null");
this.ps = con.prepareStatement(sqlStatement); //被代理对象的获取
this.sql = sqlStatement;
this.debugLevel = debugLevel;
this.formatter = formatter;
至此, DebuggableStatement 的实现方式就已经完全清晰了, 至于sql 的拼接就是在每一个set 中将具体的参数值保存在一个数组中, 最后toString 的时候, 填充即可
- SqlFormatter
- OracleSqlFormatter
这两个类只是为了实现sql 语句中, 对于日期(Date)等非基本类型的参数打印的格式化实现而已,非常简单。
完整的代码
- DebuggableStatement
class DebuggableStatement implements PreparedStatement{
private PreparedStatement ps; //preparedStatement being proxied for.
private String sql; //original statement going to database.
private String filteredSql; //statement filtered for rogue '?' that are not bind variables.
private DebugObject[] variables; //array of bind variables
private SqlFormatter formatter; //format for dates
private long startTime; //time that statement began execution
private long executeTime; //time elapsed while executing statement
private DebugLevel debugLevel; //level of debug
/**
Construct new DebugableStatement.
Uses the SqlFormatter to format date, time, timestamp outputs
@param con Connection to be used to construct PreparedStatement
@param sqlStatement sql statement to be sent to database.
@param debugLevel DebugLevel can be ON, OFF, VERBOSE.
*/
protected DebuggableStatement(Connection con, String sqlStatement, SqlFormatter formatter, DebugLevel debugLevel) throws SQLException{
//set values for member variables
if (con == null)
throw new SQLException("Connection object is null");
this.ps = con.prepareStatement(sqlStatement);
this.sql = sqlStatement;
this.debugLevel = debugLevel;
this.formatter = formatter;
//see if there are any '?' in the statement that are not bind variables
//and filter them out.
boolean isString = false;
char[] sqlString = sqlStatement.toCharArray();
for (int i = 0; i < sqlString.length; i++){
if (sqlString[i] == '\'') // 为了判断要替换的问号是否是否在单引号内部
isString = !isString;
//substitute the ? with an unprintable character if the ? is in a
//string.
if (sqlString[i] == '?' && isString) // 单引号中的问号不能替换
sqlString[i] = '\u0007';
}
filteredSql = new String(sqlString);
//find out how many variables are present in statement.
int count = 0;
int index = -1;
while ((index = filteredSql.indexOf("?",index+1)) != -1){
count++;
}
//show how many bind variables found
if (debugLevel == DebugLevel.VERBOSE)
System.out.println("count= " + count);
//create array for bind variables
variables = new DebugObject[count];
}
/**
* Facade for PreparedStatement
*/
public void addBatch() throws SQLException{
ps.addBatch();
}
/**
* Facade for PreparedStatement
*/
public void addBatch(String sql) throws SQLException{
ps.addBatch();
}
/**
* Facade for PreparedStatement
*/
public void cancel() throws SQLException{
ps.cancel();
}
/**
* Facade for PreparedStatement
*/
public void clearBatch() throws SQLException{
ps.clearBatch();
}
/**
* Facade for PreparedStatement
*/
public void clearParameters() throws SQLException{
ps.clearParameters();
}
/**
* Facade for PreparedStatement
*/
public void clearWarnings() throws SQLException{
ps.clearWarnings();
}
/**
* Facade for PreparedStatement
*/
public void close() throws SQLException{
ps.close();
}
/**
* Executes query and Calculates query execution time if DebugLevel = VERBOSE
* @return results of query
*/
public boolean execute() throws SQLException{
//execute query
Boolean results = null;
try{
results = (Boolean)executeVerboseQuery("execute",null);