/* * Copyright 2013 Sylvain LAURENT * * 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 ch.sla.jdbcperflogger.driver; import java.io.Serializable; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.ArrayList; import java.util.List; import java.util.UUID; import org.eclipse.jdt.annotation.Nullable; import ch.sla.jdbcperflogger.DatabaseType; import ch.sla.jdbcperflogger.Logger; import ch.sla.jdbcperflogger.StatementType; import ch.sla.jdbcperflogger.logger.PerfLogger; import ch.sla.jdbcperflogger.model.PreparedStatementValuesHolder; import ch.sla.jdbcperflogger.model.SqlTypedValue; public class LoggingPreparedStatementInvocationHandler extends LoggingStatementInvocationHandler { private static final String JAVA_SQL_SQL_TYPE = "java.sql.SQLType"; private static final String CLEAR_PARAMETERS = "clearParameters"; private static final Logger LOGGER = Logger.getLogger(LoggingPreparedStatementInvocationHandler.class); @Nullable private static final Method getVendorTypeNumberMethod;// for java 8 private final String rawSql; private final PreparedStatementValuesHolder paramValues = new PreparedStatementValuesHolder(); private final List<Object> batchedPreparedOrNonPreparedStmtExecutions = new ArrayList<Object>(); static { // we use tempMethod to be able to keep getVendorTypeNumberMethod final Method tempMethod = null; try { final Class<?> sqlTypeClass = Class.forName(JAVA_SQL_SQL_TYPE); tempMethod = sqlTypeClass.getMethod("getVendorTypeNumber"); } catch (final ClassNotFoundException e) { LOGGER.debug("not running under java 8"); } catch (final SecurityException e) { LOGGER.warn("Error getting getVendorTypeNumber method from java.sql.SQLType"); } catch (final NoSuchMethodException e) { LOGGER.warn("Error getting getVendorTypeNumber method from java.sql.SQLType"); } finally { getVendorTypeNumberMethod = tempMethod; } } LoggingPreparedStatementInvocationHandler(final UUID connectionId, final PreparedStatement statement, final String rawSql, final DatabaseType databaseType) { super(connectionId, statement, databaseType); this.rawSql = rawSql; } @Override @Nullable public Object invoke(final @Nullable Object proxy, final Method method, @Nullable final Object[] args) throws Throwable { final Object result; final String methodName = method.getName(); if (args == null || args.length == 0) { if (EXECUTE_QUERY.equals(methodName) && args == null) { return internalExecutePreparedQuery(method); } else if ((EXECUTE.equals(methodName) || EXECUTE_UPDATE.equals(methodName) || EXECUTE_LARGE_UPDATE.equals(methodName))) { return internalExecutePrepared(method, args); } else if (ADD_BATCH.equals(methodName)) { result = Utils.invokeUnwrapException(wrappedStatement, method, args); batchedPreparedOrNonPreparedStmtExecutions.add(paramValues.copy()); return result; } else if (CLEAR_BATCH.equals(methodName)) { result = Utils.invokeUnwrapException(wrappedStatement, method, args); batchedPreparedOrNonPreparedStmtExecutions.clear(); return result; } else if (CLEAR_PARAMETERS.equals(methodName)) { result = Utils.invokeUnwrapException(wrappedStatement, method, args); paramValues.clear(); return result; } // TODO : handle getResultSet to return a proxy to the resultset like in internalExecutePreparedQuery } else { if (methodName.startsWith("set")) { result = Utils.invokeUnwrapException(wrappedStatement, method, args); if ("setNull".equals(methodName) && args[1] instanceof Integer) { paramValues.put((Serializable) args[0], new SqlTypedValue(null, ((Integer) args[1]))); } else if (args.length == 2 || "setDate".equals(methodName) || "setTime".equals(methodName) || "setTimestamp".equals(methodName)) { paramValues.put((Serializable) args[0], new SqlTypedValue(args[1], methodName)); } else if ("setObject".equals(methodName)) { final Class<?>[] argType = method.getParameterTypes(); if (argType.length > 2) { Integer sqlType = null; if (argType[2] == Integer.TYPE) { sqlType = (Integer) args[2]; } else { // use a local variable to keep eclipse null-analysis happy final Method tempMethod = getVendorTypeNumberMethod; if (tempMethod != null && argType[2].getName().equals(JAVA_SQL_SQL_TYPE) && args[2] != null) { // for java 8... we cannot directly reference the new SQLType at compile time to retain // compatibility with java <8 sqlType = (Integer) tempMethod.invoke(args[2]); } } paramValues.put((Serializable) args[0], new SqlTypedValue(args[1], sqlType)); } } return result; } } return super.invoke(proxy, method, args); } protected ResultSet internalExecutePreparedQuery(final Method method) throws Throwable { final UUID logId = UUID.randomUUID(); PerfLogger.logBeforePreparedStatement(connectionId, logId, rawSql, paramValues, StatementType.PREPARED_QUERY_STMT, databaseType, wrappedStatement.getQueryTimeout(), wrappedStatement.getConnection().getAutoCommit()); final long start = System.nanoTime(); Throwable exc = null; try { final ResultSet resultSet = (ResultSet) Utils.invokeUnwrapExceptionReturnNonNull(wrappedStatement, method, null); return (ResultSet) Proxy.newProxyInstance(LoggingPreparedStatementInvocationHandler.class.getClassLoader(), Utils.extractAllInterfaces(resultSet.getClass()), new LoggingResultSetInvocationHandler(resultSet, logId)); } catch (final Throwable e) { exc = e; throw exc; } finally { final long end = System.nanoTime(); PerfLogger.logStatementExecuted(logId, end - start, null, exc); lastExecutionLogId = logId; } } @Nullable protected Object internalExecutePrepared(final Method method, @Nullable final Object[] args) throws Throwable { final UUID logId = UUID.randomUUID(); final long start = System.nanoTime(); PerfLogger.logBeforePreparedStatement(connectionId, logId, rawSql, paramValues, StatementType.BASE_PREPARED_STMT, databaseType, wrappedStatement.getQueryTimeout(), wrappedStatement.getConnection().getAutoCommit()); Throwable exc = null; Long updateCount = null; try { final Object result = Utils.invokeUnwrapException(wrappedStatement, method, args); if (result instanceof Number) { updateCount = ((Number) result).longValue(); } return result; } catch (final Throwable e) { exc = e; throw exc; } finally { final long end = System.nanoTime(); PerfLogger.logStatementExecuted(logId, end - start, updateCount, exc); lastExecutionLogId = logId; } } @Override @Nullable protected Object internalExecuteBatch(final Method method, @Nullable final Object[] args) throws Throwable { final UUID logId = UUID.randomUUID(); PerfLogger.logPreparedBatchedStatements(connectionId, logId, rawSql, batchedPreparedOrNonPreparedStmtExecutions, databaseType, wrappedStatement.getQueryTimeout(), wrappedStatement.getConnection().getAutoCommit()); try { return internalExecuteBatchInternal(method, args, logId); } finally { batchedPreparedOrNonPreparedStmtExecutions.clear(); lastExecutionLogId = logId; } } }