StatementDecoratorInterceptor.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.tomcat.jdbc.pool.interceptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.jdbc.pool.util.MessageFactory;
/**
* Implementation of <b>JdbcInterceptor</b> that proxies resultSets and statements.
*/
public class StatementDecoratorInterceptor extends AbstractCreateStatementInterceptor {
private static final Log logger = LogFactory.getLog(StatementDecoratorInterceptor.class);
/**
* Method name for executeQuery.
*/
protected static final String EXECUTE_QUERY = "executeQuery";
/**
* Method name for getGeneratedKeys.
*/
protected static final String GET_GENERATED_KEYS = "getGeneratedKeys";
/**
* Method name for getResultSet.
*/
protected static final String GET_RESULTSET = "getResultSet";
/**
* Method names that return a ResultSet.
*/
protected static final String[] RESULTSET_TYPES = { EXECUTE_QUERY, GET_GENERATED_KEYS, GET_RESULTSET };
/**
* Default constructor.
*/
public StatementDecoratorInterceptor() {
}
/**
* the constructor to create the resultSet proxies
*/
protected static volatile Constructor<?> resultSetConstructor = null;
@Override
public void closeInvoked() {
// Nothing to do
}
/*
* Neither the class nor the constructor are exposed outside of jdbc-pool. Given the comments in the jdbc-pool code
* regarding caching for performance, continue to use Proxy.getProxyClass(). This will need to be revisited if that
* method is marked for removal.
*/
/**
* Gets the constructor for creating ResultSet proxies.
*
* @return the ResultSet proxy constructor
*
* @throws NoSuchMethodException if the constructor is not found
*/
@SuppressWarnings("deprecation")
protected Constructor<?> getResultSetConstructor() throws NoSuchMethodException {
if (resultSetConstructor == null) {
Class<?> proxyClass = Proxy.getProxyClass(StatementDecoratorInterceptor.class.getClassLoader(),
new Class[] { ResultSet.class });
resultSetConstructor = proxyClass.getConstructor(new Class[] { InvocationHandler.class });
}
return resultSetConstructor;
}
/**
* Creates a statement interceptor to monitor query response times
*/
@Override
public Object createStatement(Object proxy, Method method, Object[] args, Object statement, long time) {
try {
String name = method.getName();
Constructor<?> constructor = null;
String sql = null;
if (compare(CREATE_STATEMENT, name)) {
// createStatement
constructor = getConstructor(CREATE_STATEMENT_IDX, Statement.class);
} else if (compare(PREPARE_STATEMENT, name)) {
// prepareStatement
constructor = getConstructor(PREPARE_STATEMENT_IDX, PreparedStatement.class);
sql = (String) args[0];
} else if (compare(PREPARE_CALL, name)) {
// prepareCall
constructor = getConstructor(PREPARE_CALL_IDX, CallableStatement.class);
sql = (String) args[0];
} else {
// Do nothing, might be a future unsupported method
// so we better bail out and let the system continue
return statement;
}
return createDecorator(proxy, method, args, statement, constructor, sql);
} catch (Exception x) {
if (x instanceof InvocationTargetException) {
Throwable cause = x.getCause();
if (cause instanceof ThreadDeath) {
throw (ThreadDeath) cause;
}
if (cause instanceof VirtualMachineError) {
throw (VirtualMachineError) cause;
}
}
logger.warn("Unable to create statement proxy for slow query report.", x);
}
return statement;
}
/**
* Creates a proxy for a Statement.
*
* @param proxy The proxy object on which the method that triggered the creation of the statement was called.
* @param method The method that was called on the proxy
* @param args The arguments passed as part of the method call to the proxy
* @param statement The statement object that is to be proxied
* @param constructor The constructor for the desired proxy
* @param sql The sql of of the statement
*
* @return A new proxy for the Statement
*
* @throws InstantiationException Couldn't instantiate object
* @throws IllegalAccessException Inaccessible constructor
* @throws InvocationTargetException Exception thrown from constructor
*/
protected Object createDecorator(Object proxy, Method method, Object[] args, Object statement,
Constructor<?> constructor, String sql)
throws InstantiationException, IllegalAccessException, InvocationTargetException {
Object result = null;
StatementProxy<Statement> statementProxy = new StatementProxy<>((Statement) statement, sql);
result = constructor.newInstance(new Object[] { statementProxy });
statementProxy.setActualProxy(result);
statementProxy.setConnection(proxy);
statementProxy.setConstructor(constructor);
return result;
}
/**
* Checks if the given method name is executeQuery.
*
* @param methodName the method name to check
*
* @return true if the method is executeQuery
*/
protected boolean isExecuteQuery(String methodName) {
return EXECUTE_QUERY.equals(methodName);
}
/**
* Checks if the given method is executeQuery.
*
* @param method the method to check
*
* @return true if the method is executeQuery
*/
protected boolean isExecuteQuery(Method method) {
return isExecuteQuery(method.getName());
}
/**
* Checks if the given method returns a ResultSet.
*
* @param method the method to check
* @param process whether processing is already enabled
*
* @return true if the method returns a ResultSet
*/
protected boolean isResultSet(Method method, boolean process) {
return process(RESULTSET_TYPES, method, process);
}
/**
* Class to measure query execute time.
*
* @param <T> The type of Statement being proxied.
*/
protected class StatementProxy<T extends Statement> implements InvocationHandler {
/**
* Indicates whether this statement has been closed. A statement can be closed from a thread other than the one
* executing on it - the statement finalizer closes tracked statements from whichever thread closes the
* connection, and the statement cache forces a close from its own purge - so the flag is written by one thread
* and read by others, and must be volatile to reach them at all.
*/
protected volatile boolean closed = false;
/**
* The delegated statement, nulled once a close has been forwarded. It is read by threads other than the one
* that nulls it, so, like {@link #closed}, volatile.
*/
protected volatile T delegate;
private Object actualProxy;
/**
* The connection handle of the borrow during which this proxy was created, bound through
* {@link #setConnection(Object)}. It is a creation-time snapshot, not a live view: once that borrow is returned
* the handle answers nothing. A component that lets a statement proxy outlive its borrow -
* {@link StatementCache} recycling a parked statement, for instance - must therefore re-bind the handle to the
* new borrower before handing the proxy out; see {@link StatementCache#invoke(Object, Method, Object[])}.
*/
private Object connection;
private String sql;
private Constructor<?> constructor;
/**
* Constructs a StatementProxy.
*
* @param delegate the statement to delegate to
* @param sql the SQL text
*/
public StatementProxy(T delegate, String sql) {
this.delegate = delegate;
this.sql = sql;
}
/**
* Returns the delegated statement.
*
* @return the delegated statement
*/
public T getDelegate() {
return this.delegate;
}
/**
* Returns the SQL text.
*
* @return the SQL text
*/
public String getSql() {
return sql;
}
/**
* Binds the connection handle that {@link #getConnection()} will report for this statement.
* {@code createDecorator} binds the handle of the borrow that creates the statement, and {@link StatementCache}
* seeds the closing borrow's handle when it parks a statement; the binding is kept as a snapshot and is never
* refreshed on its own. A handle kept past its borrow is inert by design, so anything reusing a statement proxy
* across borrows must call this method with the new borrower's handle before the proxy is handed out, as
* {@link StatementCache} does on a cache hit.
*
* @param proxy the connection proxy
*/
public void setConnection(Object proxy) {
this.connection = proxy;
}
/**
* Returns the connection proxy most recently bound through {@link #setConnection(Object)}. While that binding's
* borrow is current it is a live handle; a statement kept past its borrow answers with the disposed facade it
* came from. Once the statement is closed the handler refuses the call before it reaches the snapshot, so a
* stale binding is only visible through diagnostics such as {@link #toString()}.
*
* @return the connection proxy
*/
public Object getConnection() {
return this.connection;
}
/**
* Sets the actual proxy object.
*
* @param proxy the actual proxy
*/
public void setActualProxy(Object proxy) {
this.actualProxy = proxy;
}
/**
* Returns the actual proxy object.
*
* @return the actual proxy
*/
public Object getActualProxy() {
return this.actualProxy;
}
/**
* Returns the constructor used to create this proxy.
*
* @return the constructor
*/
public Constructor<?> getConstructor() {
return constructor;
}
/**
* Sets the constructor for creating proxies.
*
* @param constructor the constructor
*/
public void setConstructor(Constructor<?> constructor) {
this.constructor = constructor;
}
/**
* Called when the close method is invoked on the statement.
* <p>
* The driver's answer is passed on to the caller: {@code Statement.close()} declares
* {@link SQLException} for a database error while closing, and this proxy reports every other
* call as the delegate answered it, so a failed close must not come back as a success. The
* swallowed failure this replaces told the application that the statement was closed while
* the driver had left its resources on the session the pooled connection was about to hand to
* the next borrower, and nothing along the path recorded or retried it.
* <p>
* The handle is retired whichever way the driver answered: the application has been told, and
* a handle whose close failed must not go on answering calls on a statement the driver may
* have half released. Where no application waits on the close - the cache dropping a
* statement, see {@link StatementCache#closeStatement} - the failure is answered with a log
* line instead, so that one statement that will not close cannot stop the rest of that purge.
*
* @throws SQLException if the underlying statement could not be closed
*/
public void closeInvoked() throws SQLException {
// Read the delegate once: a second close running concurrently -
// the cache purge, or the application and the finalizer closing
// the same handle - can null the field between two reads, and a
// second read that finds it gone would invoke close() on a null
// receiver. A close that already completed is the one this
// snapshot answers for: the delegate is gone, nothing is
// re-closed, and the handle is retired quietly all the same.
final T target = delegate;
try {
if (target != null) {
target.close();
}
} finally {
closed = true;
delegate = null;
}
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (compare(TOSTRING_VAL, method)) {
return toString();
}
// Handle identity on this proxy, like the statement facade
// does, rather than forwarding it to the delegate: the delegate
// does not recognise the handle the application holds, and after
// close there is no delegate left to answer at all.
if (compare(EQUALS_VAL, method)) {
if (args[0] == null || !Proxy.isProxyClass(args[0].getClass())) {
return Boolean.FALSE;
}
return Boolean.valueOf(this.equals(Proxy.getInvocationHandler(args[0])));
}
if (compare(HASHCODE_VAL, method)) {
return Integer.valueOf(this.hashCode());
}
// Was close invoked?
boolean close = compare(CLOSE_VAL, method);
// Read the state once: another thread's close() - the pool's
// statement finalizer closes tracked statements from whichever
// thread closes the connection, and the statement cache forces a
// close from its purge - may complete while this call is being
// answered, and flag and delegate are two separate fields, so a
// call that read the flag as open can find the delegate already
// nulled. A close that finished in between is then answered as
// closed, on the missing delegate as much as on the flag, rather
// than by invoking on a null object: that is a
// NullPointerException, and the application is owed the
// SQLException the closed contract promises. The window this
// cannot close is the race itself: a call that reads the
// delegate while it is still live goes to it, an outcome
// indistinguishable from having simply outrun the close.
final boolean wasClosed = closed;
final T target = delegate;
final boolean gone = wasClosed || target == null;
// Allow close to be called multiple times, from either thread
if (close && gone) {
return null;
}
// Are we calling isClosed?
if (compare(ISCLOSED_VAL, method)) {
return Boolean.valueOf(gone);
}
// If we are calling anything else, bail out
if (gone) {
throw new SQLException(MessageFactory.get("statementDecoratorInterceptor.statementClosed"));
}
if (compare(GETCONNECTION_VAL, method)) {
// The handle of the current borrow, as bound by
// setConnection; a proxy kept past its borrow gets the
// inert handle it came from, and a closed one has already
// thrown above
return connection;
}
boolean process = false;
process = isResultSet(method, process);
// Check to see if we are about to execute a query
// if we are executing, get the current time
Object result = null;
try {
// Perform close cleanup
if (close) {
closeInvoked();
} else {
// Execute the query, on the delegate this call read as
// live: the snapshot keeps a concurrent close from
// replacing the receiver with null between the guard and
// the call
result = method.invoke(target, args);
}
} catch (Throwable t) {
if (t instanceof InvocationTargetException && t.getCause() != null) {
throw t.getCause();
} else {
throw t;
}
}
if (process && result != null) {
Constructor<?> cons = getResultSetConstructor();
result = cons.newInstance(new Object[] { new ResultSetProxy(actualProxy, result) });
}
return result;
}
@Override
public int hashCode() {
return System.identityHashCode(this);
}
@Override
public boolean equals(Object obj) {
return this == obj;
}
@Override
public String toString() {
StringBuffer buf = new StringBuffer(StatementProxy.class.getName());
buf.append("[Proxy=");
buf.append(System.identityHashCode(this));
buf.append("; Sql=");
buf.append(getSql());
buf.append("; Delegate=");
// Reads the live delegate rather than a snapshot: a description
// says what the proxy holds as it is asked.
buf.append(getDelegate());
buf.append("; Connection=");
buf.append(getConnection());
buf.append(']');
return buf.toString();
}
}
/**
* Invocation handler that proxies ResultSet instances.
*/
protected static class ResultSetProxy implements InvocationHandler {
private Object st;
private Object delegate;
/**
* Constructs a ResultSetProxy.
*
* @param st the statement proxy
* @param delegate the ResultSet to delegate to
*/
public ResultSetProxy(Object st, Object delegate) {
this.st = st;
this.delegate = delegate;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String name = method.getName();
// Handle the Object methods on this proxy, like the statement
// facade does, rather than forwarding them to the delegate: the
// delegate does not recognise the handle the application holds,
// and answers that leak driver details through a proxy.
if (TOSTRING_VAL.equals(name)) {
return toString();
}
if (EQUALS_VAL.equals(name)) {
if (args[0] == null || !Proxy.isProxyClass(args[0].getClass())) {
return Boolean.FALSE;
}
return Boolean.valueOf(this.equals(Proxy.getInvocationHandler(args[0])));
}
if (HASHCODE_VAL.equals(name)) {
return Integer.valueOf(this.hashCode());
}
if (GET_STATEMENT.equals(name)) {
return this.st;
} else {
try {
return method.invoke(this.delegate, args);
} catch (Throwable t) {
if (t instanceof InvocationTargetException && t.getCause() != null) {
throw t.getCause();
} else {
throw t;
}
}
}
}
@Override
public int hashCode() {
return System.identityHashCode(this);
}
@Override
public boolean equals(Object obj) {
return this == obj;
}
@Override
public String toString() {
return ResultSetProxy.class.getName() + "[Proxy=" + hashCode() + "; Delegate=" + delegate + ']';
}
}
}