/* * Copyright 2011-2017 the original author or authors. * * 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 org.glowroot.agent.plugin.jdbc; import java.sql.PreparedStatement; import java.util.List; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.glowroot.agent.plugin.api.Agent; import org.glowroot.agent.plugin.api.QueryEntry; import org.glowroot.agent.plugin.api.QueryMessageSupplier; import org.glowroot.agent.plugin.api.ThreadContext; import org.glowroot.agent.plugin.api.Timer; import org.glowroot.agent.plugin.api.TimerName; import org.glowroot.agent.plugin.api.config.BooleanProperty; import org.glowroot.agent.plugin.api.config.ConfigService; import org.glowroot.agent.plugin.api.weaving.BindParameter; import org.glowroot.agent.plugin.api.weaving.BindReceiver; import org.glowroot.agent.plugin.api.weaving.BindReturn; import org.glowroot.agent.plugin.api.weaving.BindThrowable; import org.glowroot.agent.plugin.api.weaving.BindTraveler; import org.glowroot.agent.plugin.api.weaving.IsEnabled; import org.glowroot.agent.plugin.api.weaving.Mixin; import org.glowroot.agent.plugin.api.weaving.OnAfter; import org.glowroot.agent.plugin.api.weaving.OnBefore; import org.glowroot.agent.plugin.api.weaving.OnReturn; import org.glowroot.agent.plugin.api.weaving.OnThrow; import org.glowroot.agent.plugin.api.weaving.Pointcut; import org.glowroot.agent.plugin.jdbc.PreparedStatementMirror.ByteArrayParameterValue; import org.glowroot.agent.plugin.jdbc.PreparedStatementMirror.StreamingParameterValue; import org.glowroot.agent.plugin.jdbc.message.BatchPreparedStatementMessageSupplier; import org.glowroot.agent.plugin.jdbc.message.BatchPreparedStatementMessageSupplier2; import org.glowroot.agent.plugin.jdbc.message.PreparedStatementMessageSupplier; import static java.util.concurrent.TimeUnit.MILLISECONDS; // many of the pointcuts are not restricted to configService.isEnabled() because StatementMirrors // must be tracked for their entire life public class StatementAspect { private static final String QUERY_TYPE = "SQL"; private static final ConfigService configService = Agent.getConfigService("jdbc"); private static final BooleanProperty captureBindParameters = configService.getBooleanProperty("captureBindParameters"); private static final BooleanProperty captureStatementClose = configService.getBooleanProperty("captureStatementClose"); // ===================== Mixin ===================== // the field and method names are verbose to avoid conflict since they will become fields // and methods in all classes that extend java.sql.Statement or java.sql.ResultSet @Mixin({"java.sql.Statement", "java.sql.ResultSet"}) public static class HasStatementMirrorImpl implements HasStatementMirror { // does not need to be volatile, app/framework must provide visibility of Statements and // ResultSets if used across threads and this can piggyback private @Nullable StatementMirror glowroot$statementMirror; @Override public @Nullable StatementMirror glowroot$getStatementMirror() { return glowroot$statementMirror; } @Override public void glowroot$setStatementMirror(@Nullable StatementMirror statementMirror) { this.glowroot$statementMirror = statementMirror; } @Override public boolean glowroot$hasStatementMirror() { return glowroot$statementMirror != null; } } // the method names are verbose to avoid conflict since they will become methods in all classes // that extend java.sql.Statement or java.sql.ResultSet public interface HasStatementMirror { @Nullable StatementMirror glowroot$getStatementMirror(); void glowroot$setStatementMirror(@Nullable StatementMirror statementMirror); boolean glowroot$hasStatementMirror(); } // ================= Parameter Binding ================= @Pointcut(className = "java.sql.PreparedStatement", methodName = "setArray|setBigDecimal" + "|setBoolean|setByte|setDate|setDouble|setFloat|setInt|setLong|setNString" + "|setRef|setRowId|setShort|setString|setTime|setTimestamp|setURL", methodParameterTypes = {"int", "*", ".."}) public static class SetXAdvice { @IsEnabled public static boolean isEnabled() { return captureBindParameters.value(); } @OnReturn public static void onReturn(@BindReceiver HasStatementMirror preparedStatement, @BindParameter int parameterIndex, @BindParameter @Nullable Object x) { PreparedStatementMirror mirror = (PreparedStatementMirror) preparedStatement.glowroot$getStatementMirror(); if (mirror != null) { mirror.setParameterValue(parameterIndex, x); } } } @Pointcut(className = "java.sql.PreparedStatement", methodName = "setAsciiStream|setBinaryStream|setBlob|setCharacterStream|setClob" + "|setNCharacterStream|setNClob|setSQLXML|setUnicodeStream", methodParameterTypes = {"int", "*", ".."}) public static class SetStreamAdvice { @IsEnabled public static boolean isEnabled() { return captureBindParameters.value(); } @OnReturn public static void onReturn(@BindReceiver HasStatementMirror preparedStatement, @BindParameter int parameterIndex, @BindParameter @Nullable Object x) { PreparedStatementMirror mirror = (PreparedStatementMirror) preparedStatement.glowroot$getStatementMirror(); if (mirror != null) { if (x == null) { mirror.setParameterValue(parameterIndex, null); } else { mirror.setParameterValue(parameterIndex, new StreamingParameterValue(x.getClass())); } } } } @Pointcut(className = "java.sql.PreparedStatement", methodName = "setBytes", methodParameterTypes = {"int", "byte[]"}) public static class SetBytesAdvice { @IsEnabled public static boolean isEnabled() { return captureBindParameters.value(); } @OnReturn public static void onReturn(@BindReceiver HasStatementMirror preparedStatement, @BindParameter int parameterIndex, @BindParameter byte/*@Nullable*/[] x) { PreparedStatementMirror mirror = (PreparedStatementMirror) preparedStatement.glowroot$getStatementMirror(); if (mirror != null) { if (x == null) { mirror.setParameterValue(parameterIndex, null); } else { setBytes(mirror, parameterIndex, x); } } } private static void setBytes(PreparedStatementMirror mirror, int parameterIndex, byte[] x) { boolean displayAsHex = JdbcPluginProperties.displayBinaryParameterAsHex(mirror.getSql(), parameterIndex); mirror.setParameterValue(parameterIndex, new ByteArrayParameterValue(x, displayAsHex)); } } @Pointcut(className = "java.sql.PreparedStatement", methodName = "setObject", methodParameterTypes = {"int", "java.lang.Object", ".."}) public static class SetObjectAdvice { @IsEnabled public static boolean isEnabled() { return captureBindParameters.value(); } @OnReturn public static void onReturn(@BindReceiver HasStatementMirror preparedStatement, @BindParameter int parameterIndex, @BindParameter @Nullable Object x) { PreparedStatementMirror mirror = (PreparedStatementMirror) preparedStatement.glowroot$getStatementMirror(); if (mirror != null) { if (x == null) { mirror.setParameterValue(parameterIndex, null); } else if (x instanceof byte[]) { SetBytesAdvice.setBytes(mirror, parameterIndex, (byte[]) x); } else { mirror.setParameterValue(parameterIndex, x); } } } } @Pointcut(className = "java.sql.PreparedStatement", methodName = "setNull", methodParameterTypes = {"int", "int", ".."}) public static class SetNullAdvice { @IsEnabled public static boolean isEnabled() { return captureBindParameters.value(); } @OnReturn public static void onReturn(@BindReceiver HasStatementMirror preparedStatement, @BindParameter int parameterIndex) { PreparedStatementMirror mirror = (PreparedStatementMirror) preparedStatement.glowroot$getStatementMirror(); if (mirror != null) { mirror.setParameterValue(parameterIndex, null); } } } @Pointcut(className = "java.sql.PreparedStatement", methodName = "clearParameters", methodParameterTypes = {}) public static class ClearParametersAdvice { @IsEnabled public static boolean isEnabled() { return captureBindParameters.value(); } @OnReturn public static void onReturn(@BindReceiver HasStatementMirror preparedStatement) { PreparedStatementMirror mirror = (PreparedStatementMirror) preparedStatement.glowroot$getStatementMirror(); if (mirror != null) { mirror.clearParameters(); } } } // ================== Statement Batching ================== @Pointcut(className = "java.sql.Statement", methodName = "addBatch", methodParameterTypes = {"java.lang.String"}) public static class StatementAddBatchAdvice { @OnReturn public static void onReturn(@BindReceiver HasStatementMirror statement, @BindParameter @Nullable String sql) { if (sql == null) { // seems nothing sensible to do here other than ignore return; } StatementMirror mirror = statement.glowroot$getStatementMirror(); if (mirror != null) { mirror.addBatch(sql); } } } @Pointcut(className = "java.sql.PreparedStatement", methodName = "addBatch", methodParameterTypes = {}) public static class PreparedStatementAddBatchAdvice { @OnReturn public static void onReturn(@BindReceiver HasStatementMirror preparedStatement) { PreparedStatementMirror mirror = (PreparedStatementMirror) preparedStatement.glowroot$getStatementMirror(); if (mirror != null) { mirror.addBatch(); } } } // Statement.clearBatch() can be used to re-initiate a prepared statement // that has been cached from a previous usage @Pointcut(className = "java.sql.Statement", methodName = "clearBatch", methodParameterTypes = {}) public static class ClearBatchAdvice { @OnReturn public static void onReturn(@BindReceiver HasStatementMirror statement) { StatementMirror mirror = statement.glowroot$getStatementMirror(); if (mirror != null) { mirror.clearBatch(); } } } // =================== Statement Execution =================== @Pointcut(className = "java.sql.Statement", methodName = "execute", methodParameterTypes = {"java.lang.String", ".."}, nestingGroup = "jdbc", timerName = "jdbc execute") public static class StatementExecuteAdvice { private static final TimerName timerName = Agent.getTimerName(StatementExecuteAdvice.class); @IsEnabled public static boolean isEnabled(@BindReceiver HasStatementMirror statement) { return statement.glowroot$hasStatementMirror(); } @OnBefore public static @Nullable QueryEntry onBefore(ThreadContext context, @BindReceiver HasStatementMirror statement, @BindParameter @Nullable String sql) { if (sql == null) { // seems nothing sensible to do here other than ignore return null; } StatementMirror mirror = statement.glowroot$getStatementMirror(); if (mirror == null) { // this shouldn't happen since just checked hasGlowrootStatementMirror() above return null; } QueryEntry query = context.startQueryEntry(QUERY_TYPE, sql, QueryMessageSupplier.create("jdbc execution: "), timerName); mirror.setLastQuery(query); return query; } @OnReturn public static void onReturn(@BindTraveler @Nullable QueryEntry queryEntry) { if (queryEntry != null) { queryEntry.endWithStackTrace(JdbcPluginProperties.stackTraceThresholdMillis(), MILLISECONDS); } } @OnThrow public static void onThrow(@BindThrowable Throwable t, @BindTraveler @Nullable QueryEntry queryEntry) { if (queryEntry != null) { queryEntry.endWithError(t); } } } @Pointcut(className = "java.sql.Statement", methodName = "executeQuery", methodParameterTypes = {"java.lang.String"}, methodReturnType = "java.sql.ResultSet", nestingGroup = "jdbc", timerName = "jdbc execute") public static class StatementExecuteQueryAdvice { @IsEnabled public static boolean isEnabled(@BindReceiver HasStatementMirror statement) { return statement.glowroot$hasStatementMirror(); } @OnBefore public static @Nullable QueryEntry onBefore(ThreadContext context, @BindReceiver HasStatementMirror statement, @BindParameter @Nullable String sql) { return StatementExecuteAdvice.onBefore(context, statement, sql); } @OnReturn public static void onReturn(@BindReturn @Nullable HasStatementMirror resultSet, @BindReceiver HasStatementMirror statement, @BindTraveler @Nullable QueryEntry queryEntry) { // Statement can always be retrieved from ResultSet.getStatement(), and // StatementMirror from that, but ResultSet.getStatement() is sometimes not super // duper fast due to ResultSet wrapping and other checks, so StatementMirror is // stored directly in ResultSet as an optimization if (resultSet != null) { StatementMirror mirror = statement.glowroot$getStatementMirror(); resultSet.glowroot$setStatementMirror(mirror); } if (queryEntry != null) { queryEntry.endWithStackTrace(JdbcPluginProperties.stackTraceThresholdMillis(), MILLISECONDS); } } @OnThrow public static void onThrow(@BindThrowable Throwable t, @BindTraveler @Nullable QueryEntry queryEntry) { if (queryEntry != null) { queryEntry.endWithError(t); } } } @Pointcut(className = "java.sql.Statement", methodName = "executeUpdate", methodParameterTypes = {"java.lang.String", ".."}, methodReturnType = "int", nestingGroup = "jdbc", timerName = "jdbc execute") public static class StatementExecuteUpdateAdvice { @IsEnabled public static boolean isEnabled(@BindReceiver HasStatementMirror statement) { return statement.glowroot$hasStatementMirror(); } @OnBefore public static @Nullable QueryEntry onBefore(ThreadContext context, @BindReceiver HasStatementMirror statement, @BindParameter @Nullable String sql) { return StatementExecuteAdvice.onBefore(context, statement, sql); } @OnReturn public static void onReturn(@BindReturn int rowCount, @BindTraveler @Nullable QueryEntry queryEntry) { if (queryEntry != null) { queryEntry.setCurrRow(rowCount); queryEntry.endWithStackTrace(JdbcPluginProperties.stackTraceThresholdMillis(), MILLISECONDS); } } @OnThrow public static void onThrow(@BindThrowable Throwable t, @BindTraveler @Nullable QueryEntry queryEntry) { if (queryEntry != null) { queryEntry.endWithError(t); } } } @Pointcut(className = "java.sql.PreparedStatement", methodName = "execute", methodParameterTypes = {}, nestingGroup = "jdbc", timerName = "jdbc execute") public static class PreparedStatementExecuteAdvice { private static final TimerName timerName = Agent.getTimerName(PreparedStatementExecuteAdvice.class); @IsEnabled public static boolean isEnabled(@BindReceiver HasStatementMirror preparedStatement) { return preparedStatement.glowroot$hasStatementMirror(); } @OnBefore public static QueryEntry onBefore(ThreadContext context, @BindReceiver HasStatementMirror preparedStatement) { @SuppressWarnings("nullness") // just checked above in isEnabled() @Nonnull PreparedStatementMirror mirror = (PreparedStatementMirror) preparedStatement.glowroot$getStatementMirror(); QueryMessageSupplier queryMessageSupplier; String queryText = mirror.getSql(); if (captureBindParameters.value()) { queryMessageSupplier = new PreparedStatementMessageSupplier(mirror.getParameters()); } else { queryMessageSupplier = QueryMessageSupplier.create("jdbc execution: "); } QueryEntry queryEntry = context.startQueryEntry(QUERY_TYPE, queryText, queryMessageSupplier, timerName); mirror.setLastQuery(queryEntry); return queryEntry; } @OnReturn public static void onReturn(@BindTraveler QueryEntry queryEntry) { queryEntry.endWithStackTrace(JdbcPluginProperties.stackTraceThresholdMillis(), MILLISECONDS); } @OnThrow public static void onThrow(@BindThrowable Throwable t, @BindTraveler QueryEntry queryEntry) { queryEntry.endWithError(t); } } @Pointcut(className = "java.sql.PreparedStatement", methodName = "executeQuery", methodParameterTypes = {}, methodReturnType = "java.sql.ResultSet", nestingGroup = "jdbc", timerName = "jdbc execute") public static class PreparedStatementExecuteQueryAdvice { @IsEnabled public static boolean isEnabled(@BindReceiver HasStatementMirror preparedStatement) { return preparedStatement.glowroot$hasStatementMirror(); } @OnBefore public static QueryEntry onBefore(ThreadContext context, @BindReceiver HasStatementMirror preparedStatement) { return PreparedStatementExecuteAdvice.onBefore(context, preparedStatement); } @OnReturn public static void onReturn(@BindReturn @Nullable HasStatementMirror resultSet, @BindReceiver HasStatementMirror preparedStatement, @BindTraveler QueryEntry queryEntry) { // PreparedStatement can always be retrieved from ResultSet.getStatement(), and // StatementMirror from that, but ResultSet.getStatement() is sometimes not super // duper fast due to ResultSet wrapping and other checks, so StatementMirror is // stored directly in ResultSet as an optimization if (resultSet != null) { StatementMirror mirror = preparedStatement.glowroot$getStatementMirror(); resultSet.glowroot$setStatementMirror(mirror); } queryEntry.endWithStackTrace(JdbcPluginProperties.stackTraceThresholdMillis(), MILLISECONDS); } @OnThrow public static void onThrow(@BindThrowable Throwable t, @BindTraveler QueryEntry queryEntry) { queryEntry.endWithError(t); } } @Pointcut(className = "java.sql.PreparedStatement", methodName = "executeUpdate", methodParameterTypes = {}, methodReturnType = "int", nestingGroup = "jdbc", timerName = "jdbc execute") public static class PreparedStatementExecuteUpdateAdvice { @IsEnabled public static boolean isEnabled(@BindReceiver HasStatementMirror preparedStatement) { return preparedStatement.glowroot$hasStatementMirror(); } @OnBefore public static QueryEntry onBefore(ThreadContext context, @BindReceiver HasStatementMirror preparedStatement) { return PreparedStatementExecuteAdvice.onBefore(context, preparedStatement); } @OnReturn public static void onReturn(@BindReturn int rowCount, @BindTraveler QueryEntry queryEntry) { queryEntry.setCurrRow(rowCount); queryEntry.endWithStackTrace(JdbcPluginProperties.stackTraceThresholdMillis(), MILLISECONDS); } @OnThrow public static void onThrow(@BindThrowable Throwable t, @BindTraveler QueryEntry queryEntry) { queryEntry.endWithError(t); } } @Pointcut(className = "java.sql.Statement", methodName = "executeBatch", methodParameterTypes = {}, nestingGroup = "jdbc", timerName = "jdbc execute") public static class StatementExecuteBatchAdvice { private static final TimerName timerName = Agent.getTimerName(StatementExecuteBatchAdvice.class); @IsEnabled public static boolean isEnabled(@BindReceiver HasStatementMirror statement) { return statement.glowroot$hasStatementMirror(); } @OnBefore public static QueryEntry onBefore(ThreadContext context, @BindReceiver HasStatementMirror statement) { @SuppressWarnings("nullness") // just checked above in isEnabled() @Nonnull StatementMirror mirror = statement.glowroot$getStatementMirror(); if (statement instanceof PreparedStatement) { return onBeforePreparedStatement(context, (PreparedStatementMirror) mirror); } else { return onBeforeStatement(mirror, context); } } @OnReturn public static void onReturn(@BindReturn int[] rowCounts, @BindTraveler QueryEntry queryEntry) { int totalRowCount = 0; boolean count = false; for (int rowCount : rowCounts) { if (rowCount > 0) { // ignore Statement.SUCCESS_NO_INFO (-2) and Statement.EXECUTE_FAILED (-3) totalRowCount += rowCount; count = true; } } if (count) { queryEntry.setCurrRow(totalRowCount); } queryEntry.endWithStackTrace(JdbcPluginProperties.stackTraceThresholdMillis(), MILLISECONDS); } @OnThrow public static void onThrow(@BindThrowable Throwable t, @BindTraveler QueryEntry queryEntry) { queryEntry.endWithError(t); } private static QueryEntry onBeforePreparedStatement(ThreadContext context, PreparedStatementMirror mirror) { QueryMessageSupplier queryMessageSupplier; String queryText = mirror.getSql(); int batchSize = mirror.getBatchSize(); if (captureBindParameters.value()) { queryMessageSupplier = new BatchPreparedStatementMessageSupplier( mirror.getBatchedParameters(), batchSize); } else { queryMessageSupplier = new BatchPreparedStatementMessageSupplier2(batchSize); } QueryEntry queryEntry = context.startQueryEntry(QUERY_TYPE, queryText, batchSize, queryMessageSupplier, timerName); mirror.setLastQuery(queryEntry); mirror.clearBatch(); return queryEntry; } private static QueryEntry onBeforeStatement(StatementMirror mirror, ThreadContext context) { List<String> batchedSql = mirror.getBatchedSql(); String concatenated; if (batchedSql.isEmpty()) { concatenated = "[empty batch]"; } else { StringBuilder sb = new StringBuilder("[batch] "); boolean first = true; for (String sql : batchedSql) { if (!first) { sb.append(", "); } sb.append(sql); first = false; } concatenated = sb.toString(); } QueryEntry queryEntry = context.startQueryEntry(QUERY_TYPE, concatenated, QueryMessageSupplier.create("jdbc execution: "), timerName); mirror.setLastQuery(queryEntry); mirror.clearBatch(); return queryEntry; } } // ================== Additional ResultSet Tracking ================== @Pointcut(className = "java.sql.Statement", methodName = "getResultSet|getGeneratedKeys", methodParameterTypes = {".."}, methodReturnType = "java.sql.ResultSet") public static class StatementReturnResultSetAdvice { @IsEnabled public static boolean isEnabled(@BindReceiver HasStatementMirror statement) { return statement.glowroot$hasStatementMirror(); } @OnReturn public static void onReturn(@BindReturn @Nullable HasStatementMirror resultSet, @BindReceiver HasStatementMirror statement) { if (resultSet == null) { return; } StatementMirror mirror = statement.glowroot$getStatementMirror(); resultSet.glowroot$setStatementMirror(mirror); } } // ================== Statement Closing ================== @Pointcut(className = "java.sql.Statement", methodName = "close", methodParameterTypes = {}, nestingGroup = "jdbc", timerName = "jdbc statement close") public static class CloseAdvice { private static final TimerName timerName = Agent.getTimerName(CloseAdvice.class); @IsEnabled public static boolean isEnabled(@BindReceiver HasStatementMirror statement) { return statement.glowroot$hasStatementMirror(); } @OnBefore public static @Nullable Timer onBefore(ThreadContext context, @BindReceiver HasStatementMirror statement) { StatementMirror mirror = statement.glowroot$getStatementMirror(); if (mirror != null) { // this should always be true since just checked hasGlowrootStatementMirror() above mirror.clearLastQuery(); } if (captureStatementClose.value()) { return context.startTimer(timerName); } else { return null; } } @OnAfter public static void onAfter(@BindTraveler @Nullable Timer timer) { if (timer != null) { timer.stop(); } } } }