StatementFinalizer.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.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.jdbc.pool.ConnectionPool;
import org.apache.tomcat.jdbc.pool.PoolProperties;
import org.apache.tomcat.jdbc.pool.PooledConnection;
import org.apache.tomcat.jdbc.pool.util.MessageFactory;
/**
* Keeps track of statements associated with a connection and invokes close upon {@link java.sql.Connection#close()}
* Useful for applications that don't close the associated statements after being done with a connection.
* <p>
* A statement is tracked with a strong reference until it is closed: an application that forgot about a statement is
* exactly the case the interceptor exists for, so finalisation must not depend on that statement still being reachable
* when the connection is returned. Statements that the application does close are forgotten by the returned proxy at
* the moment <code>close()</code> completes, keeping the tracked list bounded by the statements that are genuinely
* still open and closed through that handle. (The removal cannot observe a close that reaches the statement by
* another route: an interceptor placed outside this one may swallow the statement's <code>close()</code>, e.g. a
* statement cache recycling instead of closing, and a statement the application closes through the driver handle
* that <code>unwrap</code> hands back never passes through this proxy; such entries are simply reclaimed at the
* connection return, where the renewed close meets a statement already closed as a harmless no-op, as they were
* before.)
* <p>
* Tracked statements are handed to the application as proxies implementing the standard JDBC statement interfaces, as
* is the statement facade the pool always installs in front of this interceptor and as are the proxies of the other
* statement-wrapping interceptors; the driver's own statement class is therefore not reachable by casting. The
* <code>Wrapper</code> methods are delegated to the driver, and <code>unwrap</code> is the supported way to reach
* driver-specific statement methods. A statement the application reaches that way is still tracked here and is closed,
* like any other tracked statement, when the connection is returned - unless an interceptor listed after this one
* recycles the statement instead of closing it, as a statement cache beneath this interceptor does.
*/
public class StatementFinalizer extends AbstractCreateStatementInterceptor {
private static final Log log = LogFactory.getLog(StatementFinalizer.class);
/**
* Statements created on the associated connection that have not been closed yet. Every access must be guarded by
* {@link #statementsLock}.
*/
protected final List<StatementEntry> statements = new LinkedList<>();
/**
* Monitor guarding {@link #statements}. The pool's cleaner thread may clear the list (through {@link #reset}) while
* a borrower is still creating or closing statements on an abandoned connection, so subclasses that access the list
* must hold this lock as well.
*/
protected final Object statementsLock = new Object();
/**
* Default constructor.
*/
public StatementFinalizer() {
}
private volatile boolean logCreationStack = false;
@Override
public Object createStatement(Object proxy, Method method, Object[] args, Object statement, long time) {
if (!(statement instanceof Statement)) {
return statement;
}
Statement tracked = (Statement) statement;
String name = method.getName();
int constructorIndex;
Class<?> statementInterface;
if (compare(CREATE_STATEMENT, name)) {
constructorIndex = CREATE_STATEMENT_IDX;
statementInterface = Statement.class;
} else if (compare(PREPARE_STATEMENT, name)) {
constructorIndex = PREPARE_STATEMENT_IDX;
statementInterface = PreparedStatement.class;
} else if (compare(PREPARE_CALL, name)) {
constructorIndex = PREPARE_CALL_IDX;
statementInterface = CallableStatement.class;
} else {
// Not one of the statement creation methods; nothing to track
return tracked;
}
StatementEntry entry = new StatementEntry(tracked);
synchronized (statementsLock) {
statements.add(entry);
}
try {
return getConstructor(constructorIndex, statementInterface)
.newInstance(new Object[] { new CloseAwareProxy(entry) });
} catch (Exception x) {
// The statement is still tracked and will be closed when the
// connection is returned, but closing it early will no longer
// forget it before then.
log.warn(MessageFactory.get("statementFinalizer.unableToCreateTrackingProxy"), x);
return tracked;
}
}
@Override
public void closeInvoked() {
List<StatementEntry> unclosed;
synchronized (statementsLock) {
if (statements.isEmpty()) {
return;
}
unclosed = new ArrayList<>(statements);
statements.clear();
}
// Closing a statement can block on the database, so it happens
// outside the lock that guards the list.
for (StatementEntry entry : unclosed) {
entry.close();
}
}
@Override
public void setProperties(Map<String, PoolProperties.InterceptorProperty> properties) {
super.setProperties(properties);
PoolProperties.InterceptorProperty logProperty = properties.get("trace");
if (null != logProperty) {
logCreationStack = logProperty.getValueAsBoolean(logCreationStack);
}
}
@Override
public void reset(ConnectionPool parent, PooledConnection con) {
synchronized (statementsLock) {
statements.clear();
}
super.reset(parent, con);
}
/**
* Entry that tracks a statement and its allocation stack. The statement is held strongly: it must stay closeable
* until the application or the connection return disposes of it.
*/
protected class StatementEntry {
private final Statement statement;
private final Throwable allocationStack;
/**
* Creates a new StatementEntry.
*
* @param statement the statement to track
*/
public StatementEntry(Statement statement) {
this.statement = statement;
if (logCreationStack) {
this.allocationStack = new Throwable();
} else {
this.allocationStack = null;
}
}
/**
* Returns the tracked statement.
*
* @return the statement
*/
public Statement getStatement() {
return statement;
}
/**
* Returns the stack trace from when this statement was created.
*
* @return the allocation stack trace, or null if tracing is disabled
*/
public Throwable getAllocationStack() {
return allocationStack;
}
/**
* Closes the tracked statement, reporting failure the same way the connection return does.
*/
public void close() {
boolean shallClose = false;
try {
shallClose = !statement.isClosed();
} catch (Exception ignore) {
// A driver that cannot say whether the statement is closed
// must not get to skip the close attempt - the statement
// may still hold server-side state.
shallClose = true;
}
if (shallClose) {
try {
statement.close();
} catch (Exception ignore) {
if (log.isDebugEnabled()) {
log.debug("Unable to close statement upon connection close.", ignore);
}
}
}
if (logCreationStack && shallClose) {
log.warn(MessageFactory.get("statementFinalizer.statementNotClosed"), allocationStack);
}
}
}
/**
* Thin proxy around a tracked statement. Every call is delegated unchanged; only <code>close()</code> is reacted
* to, by forgetting the tracked entry so that a statement the application closed properly neither lingers in the
* list nor is touched again when the connection is returned.
* <p>
* <code>Object.equals(Object)</code> and <code>hashCode()</code> are answered on the handle itself, as
* {@link org.apache.tomcat.jdbc.pool.ProxyConnection} and the pool's other statement proxies answer them: the
* handle equals only itself and hashes at its own identity. A container that holds this handle next to an
* unwrapped handle for the same statement therefore holds two elements, whichever of the two was added first,
* instead of resolving the pair to one element or the other by insertion order. The statement underneath stays a
* distinct object that the handle compares equal to in neither direction; reaching it is <code>unwrap</code>'s
* purpose, and there the driver's own equality applies, as it does for the statement facade's handle above.
*/
protected class CloseAwareProxy implements InvocationHandler {
private final StatementEntry entry;
/**
* Creates a handler for the given tracked statement.
*
* @param entry the list entry tracking the statement
*/
public CloseAwareProxy(StatementEntry entry) {
this.entry = entry;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (compare(EQUALS_VAL, method)) {
// The identity of the handle the application holds, and
// nothing else, as ProxyConnection answers it too. Equality
// was delegated to the statement underneath, which made the
// handle equal to a statement that - answering by its own
// equality, typically identity - did not answer equal to the
// handle back. That one-sided relation is what
// Object.equals(Object) rules out, and it decided membership
// of a container holding both objects by insertion order:
// the same pair hashed alike and came out as one element or
// two depending on which was added first.
Object other = (args == null || args.length == 0) ? null : args[0];
return Boolean.valueOf(proxy == other);
}
if (compare(HASHCODE_VAL, method)) {
// The hash that goes with the identity above: the handle's
// own, so that it keeps the same place in a hash container
// whatever it is compared against.
return Integer.valueOf(System.identityHashCode(proxy));
}
Object result;
try {
result = method.invoke(entry.getStatement(), args);
} catch (InvocationTargetException ite) {
// The close failed in the statement - keep the entry so the
// connection return tries again.
Throwable cause = ite.getTargetException();
if (cause != null) {
throw cause;
}
throw ite;
}
if (compare(CLOSE_VAL, method)) {
synchronized (statementsLock) {
statements.remove(entry);
}
}
return result;
}
}
}