/*
* Copyright 2008-2017 by Emeric Vernat
*
* This file is part of Java Melody.
*
* Licensed 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 net.bull.javamelody;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.servlet.ServletContext;
import javax.sql.DataSource;
/**
* Cette classe est utile pour construire des proxy de DataSources ou de Connections jdbc.<br>
* Et notamment elle rebinde dans l'annuaire JNDI la dataSource jdbc en la remplaçant
* par un proxy de monitoring.
* @author Emeric Vernat
*/
public final class JdbcWrapper {
/**
* Instance singleton de JdbcWrapper (ici on ne connaît pas le ServletContext).
*/
public static final JdbcWrapper SINGLETON = new JdbcWrapper(
new Counter(Counter.SQL_COUNTER_NAME, "db.png"));
// au lieu d'utiliser int avec des synchronized partout, on utilise AtomicInteger
static final AtomicInteger ACTIVE_CONNECTION_COUNT = new AtomicInteger();
static final AtomicInteger USED_CONNECTION_COUNT = new AtomicInteger();
static final AtomicLong TRANSACTION_COUNT = new AtomicLong();
static final AtomicInteger ACTIVE_THREAD_COUNT = new AtomicInteger();
static final AtomicInteger RUNNING_BUILD_COUNT = new AtomicInteger();
static final AtomicInteger BUILD_QUEUE_LENGTH = new AtomicInteger();
static final Map<Integer, ConnectionInformations> USED_CONNECTION_INFORMATIONS = new ConcurrentHashMap<Integer, ConnectionInformations>();
private static final int MAX_USED_CONNECTION_INFORMATIONS = 500;
// Cette variable sqlCounter conserve un état qui est global au filtre et à l'application (donc thread-safe).
private final Counter sqlCounter;
private ServletContext servletContext;
private boolean connectionInformationsEnabled;
private boolean jboss;
private boolean glassfish;
private boolean weblogic;
static final class ConnectionInformationsComparator
implements Comparator<ConnectionInformations>, Serializable {
private static final long serialVersionUID = 1L;
/** {@inheritDoc} */
@Override
public int compare(ConnectionInformations connection1, ConnectionInformations connection2) {
return connection1.getOpeningDate().compareTo(connection2.getOpeningDate());
}
}
/**
* Handler de proxy d'un statement jdbc.
*/
private class StatementInvocationHandler implements InvocationHandler {
// Rq : dans les proxy de DataSource, Connection et Statement,
// si la méthode appelée est java.sql.Wrapper.unwrap
// on invoque toujours unwrap sur l'objet initial pour qu'il retourne lui-même
// ou son objet wrappé. Par exemple, l'appel de unwrap sur le proxy d'un Statement
// retournera le Statement initial du serveur ou même du driver bdd (OracleStatement...)
// sans notre proxy pour pouvoir appeler les méthodes non standard du driver par ex.
private String requestName;
private final Statement statement;
StatementInvocationHandler(String query, Statement statement) {
super();
assert statement != null;
this.requestName = query;
this.statement = statement;
}
/** {@inheritDoc} */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// performance : on évite method.invoke pour equals & hashCode
final String methodName = method.getName();
if (isEqualsMethod(methodName, args)) {
return statement.equals(args[0]);
} else if (isHashCodeMethod(methodName, args)) {
return statement.hashCode();
} else if (methodName.startsWith("execute")) {
if (isFirstArgAString(args)) {
// la méthode est du type executeQuery(String), executeUpdate(String),
// executeUpdate(String, ...) ou execute(String sql),
// alors la requête sql est le premier argument (et pas query)
requestName = (String) args[0];
} else if ("executeBatch".equals(methodName)
&& !requestName.startsWith("/* BATCH */ ")) {
// if executeBatch, add a prefix in the request name to explain that
// 1 batch "hit" is equivalent to several exec of the request in the db
requestName = "/* BATCH */ " + requestName;
}
// si on n'a pas trouvé la requête, on prend "null"
requestName = String.valueOf(requestName);
return doExecute(requestName, statement, method, args);
} else if ("addBatch".equals(methodName) && isFirstArgAString(args)) {
// Bien que déconseillée la méthode est addBatch(String),
// la requête sql est alors le premier argument
// (elle sera utilisée lors de l'appel à executeBatch())
// Rq : on ne conserve que la dernière requête de addBatch.
// Rq : si addBatch(String) est appelée, puis que executeUpdate(String)
// la requête du batch est correctement ignorée ci-dessus.
// Rq : si connection.prepareStatement(String).addBatch(String) puis executeUpdate()
// sont appelées (et pas executeBatch()) alors la requête conservée est
// faussement celle du batch mais l'application cloche grave.
requestName = (String) args[0];
}
// ce n'est pas une méthode executeXxx du Statement
return method.invoke(statement, args);
}
private boolean isFirstArgAString(Object[] args) {
return args != null && args.length > 0 && args[0] instanceof String;
}
}
/**
* Handler de proxy d'une connexion jdbc.
*/
private class ConnectionInvocationHandler implements InvocationHandler {
private final Connection connection;
private boolean alreadyClosed;
ConnectionInvocationHandler(Connection connection) {
super();
assert connection != null;
this.connection = connection;
}
void init() {
// on limite la taille pour éviter une éventuelle saturation mémoire
if (isConnectionInformationsEnabled()
&& USED_CONNECTION_INFORMATIONS.size() < MAX_USED_CONNECTION_INFORMATIONS) {
USED_CONNECTION_INFORMATIONS.put(
ConnectionInformations.getUniqueIdOfConnection(connection),
new ConnectionInformations());
}
USED_CONNECTION_COUNT.incrementAndGet();
TRANSACTION_COUNT.incrementAndGet();
}
/** {@inheritDoc} */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// performance : on évite method.invoke pour equals & hashCode
final String methodName = method.getName();
if (isEqualsMethod(methodName, args)) {
return areConnectionsEquals(args[0]);
} else if (isHashCodeMethod(methodName, args)) {
return connection.hashCode();
}
try {
Object result = method.invoke(connection, args);
if (result instanceof Statement) {
final String requestName;
if ("prepareStatement".equals(methodName) || "prepareCall".equals(methodName)) {
// la méthode est du type prepareStatement(String) ou prepareCall(String),
// alors la requête sql est le premier argument
requestName = (String) args[0];
} else {
requestName = null;
}
result = createStatementProxy(requestName, (Statement) result);
}
return result;
} finally {
if ("close".equals(methodName) && !alreadyClosed) {
USED_CONNECTION_COUNT.decrementAndGet();
USED_CONNECTION_INFORMATIONS
.remove(ConnectionInformations.getUniqueIdOfConnection(connection));
alreadyClosed = true;
}
}
}
private boolean areConnectionsEquals(Object object) {
// Special case if what we're being passed is one of our proxies (specifically a connection proxy)
// This way the equals call is truely transparent for our proxies (cf issue 78)
if (Proxy.isProxyClass(object.getClass())) {
final InvocationHandler invocationHandler = Proxy.getInvocationHandler(object);
if (invocationHandler instanceof DelegatingInvocationHandler) {
final DelegatingInvocationHandler d = (DelegatingInvocationHandler) invocationHandler;
if (d.getDelegate() instanceof ConnectionInvocationHandler) {
final ConnectionInvocationHandler c = (ConnectionInvocationHandler) d
.getDelegate();
return connection.equals(c.connection);
}
}
}
return connection.equals(object);
}
}
private static class ConnectionManagerInvocationHandler
extends AbstractInvocationHandler<Object> {
// classe sérialisable pour glassfish v2.1.1, issue 229: Exception in NamingManagerImpl copyMutableObject()
private static final long serialVersionUID = 1L;
ConnectionManagerInvocationHandler(Object javaxConnectionManager) {
super(javaxConnectionManager);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
final Object result = method.invoke(getProxiedObject(), args);
if (result instanceof Connection) {
SINGLETON.createConnectionProxyOrRewrapIfJBossOrGlassfish((Connection) result);
}
return result;
}
}
private abstract static class AbstractInvocationHandler<T>
implements InvocationHandler, Serializable {
private static final long serialVersionUID = 1L;
@SuppressWarnings("all")
private final T proxiedObject;
AbstractInvocationHandler(T proxiedObject) {
super();
this.proxiedObject = proxiedObject;
}
T getProxiedObject() {
return proxiedObject;
}
}
// ce handler désencapsule les InvocationTargetException des proxy
private static class DelegatingInvocationHandler implements InvocationHandler, Serializable {
// classe sérialisable pour MonitoringProxy
private static final long serialVersionUID = 7515240588169084785L;
@SuppressWarnings("all")
private final InvocationHandler delegate;
DelegatingInvocationHandler(InvocationHandler delegate) {
super();
this.delegate = delegate;
}
InvocationHandler getDelegate() {
return delegate;
}
/** {@inheritDoc} */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
return delegate.invoke(proxy, method, args);
} catch (final InvocationTargetException e) {
if (e.getTargetException() != null) {
throw e.getTargetException();
}
throw e;
}
}
}
private JdbcWrapper(Counter sqlCounter) {
super();
assert sqlCounter != null;
this.sqlCounter = sqlCounter;
// servletContext reste null pour l'instant
this.servletContext = null;
connectionInformationsEnabled = Parameters.isSystemActionsEnabled()
&& !Parameters.isNoDatabase();
}
void initServletContext(ServletContext context) {
assert context != null;
this.servletContext = context;
final String serverInfo = servletContext.getServerInfo();
jboss = serverInfo.contains("JBoss") || serverInfo.contains("WildFly");
glassfish = serverInfo.contains("GlassFish")
|| serverInfo.contains("Sun Java System Application Server")
|| serverInfo.contains("Payara");
weblogic = serverInfo.contains("WebLogic");
connectionInformationsEnabled = Parameters.isSystemActionsEnabled()
&& !Parameters.isNoDatabase();
}
static int getUsedConnectionCount() {
return USED_CONNECTION_COUNT.get();
}
static int getActiveConnectionCount() {
return ACTIVE_CONNECTION_COUNT.get();
}
static long getTransactionCount() {
return TRANSACTION_COUNT.get();
}
static int getActiveThreadCount() {
return ACTIVE_THREAD_COUNT.get();
}
static int getRunningBuildCount() {
return RUNNING_BUILD_COUNT.get();
}
static int getBuildQueueLength() {
return BUILD_QUEUE_LENGTH.get();
}
static List<ConnectionInformations> getConnectionInformationsList() {
final List<ConnectionInformations> result = new ArrayList<ConnectionInformations>(
USED_CONNECTION_INFORMATIONS.values());
Collections.sort(result, new ConnectionInformationsComparator());
return Collections.unmodifiableList(result);
}
Counter getSqlCounter() {
return sqlCounter;
}
boolean isConnectionInformationsEnabled() {
return connectionInformationsEnabled;
}
static int getMaxConnectionCount() {
return JdbcWrapperHelper.getMaxConnectionCount();
}
static Map<String, Map<String, Object>> getBasicDataSourceProperties() {
return JdbcWrapperHelper.getBasicDataSourceProperties();
}
static Map<String, DataSource> getJndiAndSpringDataSources() throws NamingException {
return JdbcWrapperHelper.getJndiAndSpringDataSources();
}
/**
* Enregistre une DataSource ne venant pas de JNDI.
* @param name String
* @param dataSource DataSource
*/
public static void registerSpringDataSource(String name, DataSource dataSource) {
JdbcWrapperHelper.registerSpringDataSource(name, dataSource);
}
Object doExecute(String requestName, Statement statement, Method method, Object[] args)
throws IllegalAccessException, InvocationTargetException {
assert requestName != null;
assert statement != null;
assert method != null;
// on ignore les requêtes explain exécutées par DatabaseInformations
if (!sqlCounter.isDisplayed() || requestName.startsWith("explain ")) {
ACTIVE_CONNECTION_COUNT.incrementAndGet();
try {
return method.invoke(statement, args);
} finally {
ACTIVE_CONNECTION_COUNT.decrementAndGet();
}
}
final long start = System.currentTimeMillis();
boolean systemError = true;
try {
ACTIVE_CONNECTION_COUNT.incrementAndGet();
// note perf: selon un paramètre current-sql(/requests)-disabled,
// on pourrait ici ne pas binder un nouveau contexte à chaque requête sql
sqlCounter.bindContext(requestName, requestName, null, -1);
final Object result = method.invoke(statement, args);
systemError = false;
return result;
} catch (final InvocationTargetException e) {
if (e.getCause() instanceof SQLException) {
final int errorCode = ((SQLException) e.getCause()).getErrorCode();
if (errorCode >= 20000 && errorCode < 30000) {
// Dans Oracle par exemple, les erreurs 20000 à 30000 sont standardisées
// comme étant des erreurs lancées par l'application dans des procédures stockées
// pour être traitées comme des erreurs de saisies ou comme des règles de gestion.
// Ce ne sont donc pas des erreurs systèmes.
systemError = false;
}
}
throw e;
} finally {
// Rq : on n'utilise pas la création du statement et l'appel à la méthode close du statement
// comme début et fin d'une connexion active, car en fonction de l'application
// la méthode close du statement peut ne jamais être appelée
// (par exemple, seule la méthode close de la connection peut être appelée ce qui ferme aussi le statement)
// Rq : pas de temps cpu pour les requêtes sql car c'est 0 ou quasiment 0
ACTIVE_CONNECTION_COUNT.decrementAndGet();
final long duration = Math.max(System.currentTimeMillis() - start, 0);
sqlCounter.addRequest(requestName, duration, -1, systemError, -1);
}
}
boolean rebindDataSources() {
boolean ok;
// on cherche une datasource avec InitialContext pour afficher nom et version bdd + nom et version driver jdbc
// (le nom de la dataSource recherchée dans JNDI est du genre jdbc/Xxx qui est le nom standard d'une DataSource)
try {
final boolean rewrapDataSources = Boolean
.parseBoolean(Parameters.getParameter(Parameter.REWRAP_DATASOURCES));
if (rewrapDataSources) {
// on annule le rebinding éventuellement fait avant par SessionListener
// si rewrap-datasources est défini dans le filter
JdbcWrapperHelper.rebindInitialDataSources(servletContext);
}
final Map<String, DataSource> jndiDataSources = JdbcWrapperHelper.getJndiDataSources();
LOG.debug("datasources found in JNDI: " + jndiDataSources.keySet());
for (final Map.Entry<String, DataSource> entry : jndiDataSources.entrySet()) {
final String jndiName = entry.getKey();
final DataSource dataSource = entry.getValue();
try {
if (rewrapDataSources || isServerNeedsRewrap(jndiName)) {
rewrapDataSource(jndiName, dataSource);
} else if (!isProxyAlready(dataSource)) {
// si dataSource est déjà un proxy, il ne faut pas faire un proxy d'un proxy ni un rebinding
final DataSource dataSourceProxy = createDataSourceProxy(jndiName,
dataSource);
JdbcWrapperHelper.rebindDataSource(servletContext, jndiName, dataSource,
dataSourceProxy);
LOG.debug("datasource rebinded: " + jndiName + " from class "
+ dataSource.getClass().getName() + " to class "
+ dataSourceProxy.getClass().getName());
}
} catch (final Throwable t) { // NOPMD
// ça n'a pas marché, tant pis pour celle-ci qui semble invalide, mais continuons avec les autres
LOG.debug("rebinding datasource " + jndiName + " failed, skipping it", t);
}
}
ok = true;
} catch (final Throwable t) { // NOPMD
// ça n'a pas marché, tant pis
LOG.debug("rebinding datasources failed, skipping", t);
ok = false;
}
return ok;
}
private void rewrapDataSource(String jndiName, DataSource dataSource)
throws IllegalAccessException {
final String dataSourceClassName = dataSource.getClass().getName();
LOG.debug("Datasource needs rewrap: " + jndiName + " of class " + dataSourceClassName);
final String dataSourceRewrappedMessage = "Datasource rewrapped: " + jndiName;
if (isJBossOrGlassfishDataSource(dataSourceClassName)) {
// JBOSS: le rebind de la datasource dans le JNDI JBoss est possible mais ne
// fonctionne pas (car tous les lookup renverraient alors une instance de
// MarshalledValuePair ou une instance javax.naming.Reference selon comment cela
// est fait), donc on modifie directement l'instance de WrapperDataSource déjà
// présente dans le JNDI.
// GLASSFISH: le contexte JNDI commençant par "java:" est en lecture seule
// dans glassfish (comme dit dans la spec et comme implémenté dans
// http://kickjava.com/src/com/sun/enterprise/naming/java/javaURLContext.java.htm),
// donc on modifie directement l'instance de DataSource40 déjà présente dans le
// JNDI.
// Par "chance", la classe org.jboss.resource.adapter.jdbc.WrapperDataSource et
// la super-classe de com.sun.gjc.spi.jdbc40.DataSource40 contiennent toutes les
// deux un attribut de nom "cm" et de type javax.resource.spi.ConnectionManager
// dont on veut faire un proxy.
Object javaxConnectionManager = JdbcWrapperHelper.getFieldValue(dataSource, "cm");
javaxConnectionManager = createJavaxConnectionManagerProxy(javaxConnectionManager);
JdbcWrapperHelper.setFieldValue(dataSource, "cm", javaxConnectionManager);
LOG.debug(dataSourceRewrappedMessage);
} else if (isWildfly9DataSource(dataSourceClassName)) {
Object delegateDataSource = JdbcWrapperHelper.getFieldValue(dataSource, "delegate");
delegateDataSource = createDataSourceProxy((DataSource) delegateDataSource);
JdbcWrapperHelper.setFieldValue(dataSource, "delegate", delegateDataSource);
LOG.debug(dataSourceRewrappedMessage);
} else if (weblogic
&& "weblogic.jdbc.common.internal.RmiDataSource".equals(dataSourceClassName)) {
// WEBLOGIC: le contexte JNDI est en lecture seule donc on modifie directement
// l'instance de RmiDataSource déjà présente dans le JNDI.
rewrapWebLogicDataSource(dataSource);
LOG.debug(dataSourceRewrappedMessage);
} else if (isDbcpDataSource(dataSourceClassName)) {
// JIRA dans Tomcat: la dataSource a déjà été mise en cache par org.ofbiz.core.entity.transaction.JNDIFactory
// à l'initialisation de com.atlassian.jira.startup.JiraStartupChecklistContextListener
// donc on modifie directement l'instance de BasicDataSource déjà présente dans le JNDI.
// Et dans certains JIRA la datasource est bien une instance de org.apache.commons.dbcp.BasicDataSource
// cf http://groups.google.com/group/javamelody/browse_thread/thread/da8336b908f1e3bd/6cf3048f1f11866e?show_docid=6cf3048f1f11866e
// et aussi rewrap pour tomee/openejb (cf issue 104),
rewrapBasicDataSource(dataSource);
LOG.debug(dataSourceRewrappedMessage);
} else if ("org.apache.openejb.resource.jdbc.managed.local.ManagedDataSource"
.equals(dataSourceClassName)) {
// rewrap pour tomee/openejb plus récents (cf issue 104),
rewrapTomEEDataSource(dataSource);
LOG.debug(dataSourceRewrappedMessage);
} else {
LOG.info("Datasource can't be rewrapped: " + jndiName + " of class "
+ dataSourceClassName);
}
}
private boolean isServerNeedsRewrap(String jndiName) {
return glassfish || jboss || weblogic || jndiName.contains("openejb");
}
private boolean isDbcpDataSource(String dataSourceClassName) {
return "org.apache.tomcat.dbcp.dbcp.BasicDataSource".equals(dataSourceClassName)
|| "org.apache.tomcat.dbcp.dbcp2.BasicDataSource".equals(dataSourceClassName)
|| "org.apache.commons.dbcp.BasicDataSource".equals(dataSourceClassName)
|| "org.apache.commons.dbcp2.BasicDataSource".equals(dataSourceClassName)
|| "org.apache.openejb.resource.jdbc.BasicManagedDataSource"
.equals(dataSourceClassName)
|| "org.apache.openejb.resource.jdbc.BasicDataSource".equals(dataSourceClassName);
}
private boolean isJBossOrGlassfishDataSource(String dataSourceClassName) {
return jboss
&& "org.jboss.resource.adapter.jdbc.WrapperDataSource".equals(dataSourceClassName)
|| jboss && "org.jboss.jca.adapters.jdbc.WrapperDataSource"
.equals(dataSourceClassName)
|| glassfish && "com.sun.gjc.spi.jdbc40.DataSource40".equals(dataSourceClassName);
}
private boolean isWildfly9DataSource(String dataSourceClassName) {
return jboss && "org.jboss.as.connector.subsystems.datasources.WildFlyDataSource"
.equals(dataSourceClassName);
}
private void rewrapWebLogicDataSource(DataSource dataSource) throws IllegalAccessException {
Object jdbcCtx = JdbcWrapperHelper.getFieldValue(dataSource, "jdbcCtx");
if (jdbcCtx != null) {
jdbcCtx = createContextProxy((Context) jdbcCtx);
JdbcWrapperHelper.setFieldValue(dataSource, "jdbcCtx", jdbcCtx);
}
Object driverInstance = JdbcWrapperHelper.getFieldValue(dataSource, "driverInstance");
if (driverInstance != null) {
driverInstance = createDriverProxy((Driver) driverInstance);
JdbcWrapperHelper.setFieldValue(dataSource, "driverInstance", driverInstance);
}
}
private void rewrapBasicDataSource(DataSource dataSource) throws IllegalAccessException {
// on récupère une connection avant de la refermer,
// car sinon la datasource interne n'est pas encore créée
// et le rewrap ne peut pas fonctionner
try {
dataSource.getConnection().close();
} catch (final Exception e) {
LOG.debug(e.toString());
// ce n'est pas grave s'il y a une exception, par exemple parce que la base n'est pas disponible,
// car l'essentiel est de créer la datasource
}
Object innerDataSource = JdbcWrapperHelper.getFieldValue(dataSource, "dataSource");
if (innerDataSource != null) {
innerDataSource = createDataSourceProxy((DataSource) innerDataSource);
JdbcWrapperHelper.setFieldValue(dataSource, "dataSource", innerDataSource);
}
}
private void rewrapTomEEDataSource(DataSource dataSource) throws IllegalAccessException {
// on récupère une connection avant de la refermer,
// car sinon la datasource interne n'est pas encore créée
// et le rewrap ne peut pas fonctionner
try {
dataSource.getConnection().close();
} catch (final Exception e) {
LOG.debug(e.toString());
// ce n'est pas grave s'il y a une exception, par exemple parce que la base n'est pas disponible,
// car l'essentiel est de créer la datasource
}
Object innerDataSource = JdbcWrapperHelper.getFieldValue(dataSource, "delegate");
if (innerDataSource != null) {
innerDataSource = createDataSourceProxy((DataSource) innerDataSource);
JdbcWrapperHelper.setFieldValue(dataSource, "delegate", innerDataSource);
}
}
boolean stop() {
boolean ok;
try {
JdbcWrapperHelper.rebindInitialDataSources(servletContext);
// si jboss, glassfish ou weblogic avec datasource, on désencapsule aussi les objets wrappés
final Map<String, DataSource> jndiDataSources = JdbcWrapperHelper.getJndiDataSources();
final boolean rewrapDataSources = Boolean
.parseBoolean(Parameters.getParameter(Parameter.REWRAP_DATASOURCES));
for (final Map.Entry<String, DataSource> entry : jndiDataSources.entrySet()) {
final String jndiName = entry.getKey();
final DataSource dataSource = entry.getValue();
if (rewrapDataSources || isServerNeedsRewrap(jndiName)) {
unwrapDataSource(jndiName, dataSource);
}
}
JdbcWrapperHelper.clearProxyCache();
ok = true;
} catch (final Throwable t) { // NOPMD
// ça n'a pas marché, tant pis
LOG.debug("rebinding initial datasources failed, skipping", t);
ok = false;
}
return ok;
}
private void unwrapDataSource(String jndiName, DataSource dataSource)
throws IllegalAccessException {
final String dataSourceClassName = dataSource.getClass().getName();
LOG.debug("Datasource needs unwrap: " + jndiName + " of class " + dataSourceClassName);
final String dataSourceUnwrappedMessage = "Datasource unwrapped: " + jndiName;
if (isJBossOrGlassfishDataSource(dataSourceClassName)) {
unwrap(dataSource, "cm", dataSourceUnwrappedMessage);
} else if (isWildfly9DataSource(dataSourceClassName)) {
unwrap(dataSource, "delegate", dataSourceUnwrappedMessage);
} else if (weblogic
&& "weblogic.jdbc.common.internal.RmiDataSource".equals(dataSourceClassName)) {
unwrap(dataSource, "jdbcCtx", dataSourceUnwrappedMessage);
unwrap(dataSource, "driverInstance", dataSourceUnwrappedMessage);
} else if (isDbcpDataSource(dataSourceClassName)) {
unwrap(dataSource, "dataSource", dataSourceUnwrappedMessage);
}
}
private void unwrap(Object parentObject, String fieldName, String unwrappedMessage)
throws IllegalAccessException {
final Object proxy = JdbcWrapperHelper.getFieldValue(parentObject, fieldName);
if (Proxy.isProxyClass(proxy.getClass())) {
InvocationHandler invocationHandler = Proxy.getInvocationHandler(proxy);
if (invocationHandler instanceof DelegatingInvocationHandler) {
invocationHandler = ((DelegatingInvocationHandler) invocationHandler).getDelegate();
if (invocationHandler instanceof AbstractInvocationHandler) {
final Object proxiedObject = ((AbstractInvocationHandler<?>) invocationHandler)
.getProxiedObject();
JdbcWrapperHelper.setFieldValue(parentObject, fieldName, proxiedObject);
LOG.debug(unwrappedMessage);
}
}
}
}
Context createContextProxy(final Context context) {
assert context != null;
final InvocationHandler invocationHandler = new AbstractInvocationHandler<Context>(
context) {
private static final long serialVersionUID = 1L;
/** {@inheritDoc} */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(context, args);
if (result instanceof DataSource) {
result = createDataSourceProxy((DataSource) result);
}
return result;
}
};
return createProxy(context, invocationHandler);
}
// pour weblogic
private Driver createDriverProxy(final Driver driver) {
assert driver != null;
final InvocationHandler invocationHandler = new AbstractInvocationHandler<Driver>(driver) {
private static final long serialVersionUID = 1L;
/** {@inheritDoc} */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(driver, args);
if (result instanceof Connection) {
result = createConnectionProxy((Connection) result);
}
return result;
}
};
return createProxy(driver, invocationHandler);
}
// pour jboss ou glassfish
private Object createJavaxConnectionManagerProxy(Object javaxConnectionManager) {
assert javaxConnectionManager != null;
final InvocationHandler invocationHandler = new ConnectionManagerInvocationHandler(
javaxConnectionManager);
return createProxy(javaxConnectionManager, invocationHandler);
}
void rewrapConnection(Connection connection) throws IllegalAccessException {
assert connection != null;
if (jboss && connection.getClass().getSimpleName().startsWith("WrappedConnection")) {
// pour jboss,
// result instance de WrappedConnectionJDK6 ou WrappedConnectionJDK5
// (attribut "mc" sur classe parente)
final Object baseWrapperManagedConnection = JdbcWrapperHelper.getFieldValue(connection,
"mc");
final String conFieldName = "con";
Connection con = (Connection) JdbcWrapperHelper
.getFieldValue(baseWrapperManagedConnection, conFieldName);
// on teste isProxyAlready ici pour raison de perf
if (!isProxyAlready(con)) {
con = createConnectionProxy(con);
JdbcWrapperHelper.setFieldValue(baseWrapperManagedConnection, conFieldName, con);
}
} else if (glassfish && ("com.sun.gjc.spi.jdbc40.ConnectionHolder40"
.equals(connection.getClass().getName())
|| "com.sun.gjc.spi.jdbc40.ConnectionWrapper40"
.equals(connection.getClass().getName()))) {
// pour glassfish,
// result instance de com.sun.gjc.spi.jdbc40.ConnectionHolder40
// ou com.sun.gjc.spi.jdbc40.ConnectionWrapper40 selon message dans users' group
// (attribut "con" sur classe parente)
final String conFieldName = "con";
Connection con = (Connection) JdbcWrapperHelper.getFieldValue(connection, conFieldName);
// on teste isProxyAlready ici pour raison de perf
if (!isProxyAlready(con)) {
con = createConnectionProxy(con);
JdbcWrapperHelper.setFieldValue(connection, conFieldName, con);
}
}
}
/**
* Crée un proxy d'une dataSource jdbc.
* @param dataSource DataSource
* @return DataSource
*/
public DataSource createDataSourceProxy(DataSource dataSource) {
return createDataSourceProxy(null, dataSource);
}
/**
* Crée un proxy d'une dataSource jdbc.
* @param name String
* @param dataSource DataSource
* @return DataSource
*/
public DataSource createDataSourceProxy(String name, final DataSource dataSource) {
assert dataSource != null;
JdbcWrapperHelper.pullDataSourceProperties(name, dataSource);
final InvocationHandler invocationHandler = new AbstractInvocationHandler<DataSource>(
dataSource) {
private static final long serialVersionUID = 1L;
/** {@inheritDoc} */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(dataSource, args);
if (result instanceof Connection) {
result = createConnectionProxy((Connection) result);
}
return result;
}
};
return createProxy(dataSource, invocationHandler);
}
Connection createConnectionProxyOrRewrapIfJBossOrGlassfish(Connection connection)
throws IllegalAccessException {
if (jboss || glassfish) {
rewrapConnection(connection);
return connection;
}
return createConnectionProxy(connection);
}
/**
* Crée un proxy d'une connexion jdbc.
* @param connection Connection
* @return Connection
*/
public Connection createConnectionProxy(Connection connection) {
assert connection != null;
// même si le counter sql n'est pas affiché on crée un proxy de la connexion
// pour avoir les graphiques USED_CONNECTION_COUNT et ACTIVE_CONNECTION_COUNT (cf issue 160)
if (isMonitoringDisabled()) {
return connection;
}
final ConnectionInvocationHandler invocationHandler = new ConnectionInvocationHandler(
connection);
final Connection result = createProxy(connection, invocationHandler);
if (result != connection) {
invocationHandler.init();
}
return result;
}
boolean isSqlMonitoringDisabled() {
return isMonitoringDisabled() || !sqlCounter.isDisplayed();
}
private static boolean isMonitoringDisabled() {
// on doit réévaluer ici le paramètre, car au départ le servletContext
// n'est pas forcément défini si c'est un driver jdbc sans dataSource
return Boolean.parseBoolean(Parameters.getParameter(Parameter.DISABLED));
}
Statement createStatementProxy(String query, Statement statement) {
assert statement != null;
// Si un proxy de connexion a été créé dans un driver jdbc et que par la suite le
// servletContext a un paramètre désactivant le monitoring, alors ce n'est pas grave
// les requêtes sql seront simplement agrégées dans le counter pour cette connexion
// jusqu'à sa fermeture (pour éviter ce détail, il suffirait simplement d'utiliser une
// dataSource jdbc et pas un driver).
// Rq : on ne réévalue pas le paramètre ici pour raison de performances sur la recherche
// dans les paramètres du système, du contexte et du filtre alors que dans 99.999999999%
// des exécutions il n'y a pas le paramètre.
final InvocationHandler invocationHandler = new StatementInvocationHandler(query,
statement);
return createProxy(statement, invocationHandler);
}
static boolean isEqualsMethod(Object methodName, Object[] args) {
// == for perf (strings interned: == is ok)
return "equals" == methodName && args != null && args.length == 1; // NOPMD
}
static boolean isHashCodeMethod(Object methodName, Object[] args) {
// == for perf (strings interned: == is ok)
return "hashCode" == methodName && (args == null || args.length == 0); // NOPMD
}
static <T> T createProxy(T object, InvocationHandler invocationHandler) {
return createProxy(object, invocationHandler, null);
}
static <T> T createProxy(T object, InvocationHandler invocationHandler,
List<Class<?>> interfaces) {
if (isProxyAlready(object)) {
// si l'objet est déjà un proxy créé pas nous, initialisé par exemple
// depuis SessionListener ou MonitoringInitialContextFactory,
// alors il ne faut pas faire un proxy du proxy
return object;
}
final InvocationHandler ih = new DelegatingInvocationHandler(invocationHandler);
return JdbcWrapperHelper.createProxy(object, ih, interfaces);
}
private static boolean isProxyAlready(Object object) {
return Proxy.isProxyClass(object.getClass()) && Proxy.getInvocationHandler(object)
.getClass().getName().equals(DelegatingInvocationHandler.class.getName());
// utilisation de Proxy.getInvocationHandler(object).getClass().getName().equals(DelegatingInvocationHandler.class.getName())
// et non de Proxy.getInvocationHandler(object) instanceof DelegatingInvocationHandler
// pour issue 97 (classLoaders différents pour les classes DelegatingInvocationHandler)
}
}