title: How to intercept inherited method
body: <p>I want to build a Java agent with Byte Buddy, which intercepts JDBC connection creation and closing event. I want to support data sources such as HikariCP, DBCP, etc, also plain JDBC with RDBMS drivers.</p>
  <p>My code:</p>
  <h3>Instrumention setup</h3>
  <pre class="lang-java prettyprint-override"><code>new AgentBuilder.Default()
                  .type(startMatcher.and(isSubTypeOf(java.sql.Connection.class).and(not(isAbstract())))
                  .transform(constructorTransformer).transform(methodsTransformer).with(listener).installOn(inst);
  </code></pre>
  <h3>Intercepting code</h3>
  <pre class="lang-java prettyprint-override"><code>package org.wxt.xtools.agents.jdbcmon;
  
  import java.io.IOException;
  import java.io.PrintWriter;
  import java.io.StringWriter;
  import java.lang.reflect.Constructor;
  import java.lang.reflect.Method;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.concurrent.Callable;
  
  import org.wxt.xtools.agents.utils.StringUtils;
  import org.wxt.xtools.agents.utils.TopN;
  
  import ch.qos.logback.classic.Logger;
  import io.prometheus.client.Histogram;
  import net.bytebuddy.asm.Advice;
  import net.bytebuddy.asm.Advice.This;
  import net.bytebuddy.implementation.bind.annotation.AllArguments;
  import net.bytebuddy.implementation.bind.annotation.Origin;
  import net.bytebuddy.implementation.bind.annotation.RuntimeType;
  import net.bytebuddy.implementation.bind.annotation.SuperCall;
  
  /**
   * 
   * @author ggfan
   * 
   */
  public class JDBCAPIInterceptor {
  
      // ***ATTENTION*** fields below must be public, as we will access them from the
      // intercepting methods
      public static Map&lt;Integer, ConnectionInfo&gt; conns = new HashMap&lt;Integer, ConnectionInfo&gt;();
      
      public static Map&lt;String, String&gt; stacks = new HashMap&lt;String, String&gt;();
  
      public static Map&lt;String, Integer&gt; counstsByStack = new HashMap&lt;String, Integer&gt;();
      
      public static TopN&lt;StatementExecutionInfo&gt; slowStatements = new TopN&lt;StatementExecutionInfo&gt;(10, new Comparator&lt;StatementExecutionInfo&gt;() {
          @Override
          public int compare(StatementExecutionInfo o1, StatementExecutionInfo o2) {
              long d1 = o1.getExecutionEndTime() - o1.getExecutionStartTime();
              long d2 = o2.getExecutionEndTime() - o2.getExecutionStartTime();
              return Long.compare(d1, d2);
          }
      });
  
      public static Logger log = JDBCMonitor.log;
  
      public static JDBCStatsReport getInfo() {
          JDBCStatsReport report = new JDBCStatsReport();
          List&lt;ConnectionInfo&gt; data = new ArrayList&lt;ConnectionInfo&gt;();
          for (ConnectionInfo ci : conns.values()) {
              ConnectionInfo copy = new ConnectionInfo();
              copy.setHash(ci.getHash());
              copy.setStackHash(ci.getStackHash() + &quot;(&quot; + counstsByStack.get(ci.getStackHash()) + &quot;)&quot;);
              copy.setCreationTime(ci.getCreationTime());
              copy.setLastActiveTime(ci.getLastActiveTime());
              copy.setLastMethod(ci.getLastMethod());
              copy.setCurrentStatement(ci.getCurrentStatement());
              data.add(copy);                                                                                                                                                                                                                                                                
          }
          report.setConnectionInfo(data);
          report.setSlowStatementTopN(slowStatements);
          return report;
      }
  
      @RuntimeType
      public static Object interceptor(@Origin Class&lt;?&gt; clazz, @Origin Method method, @SuperCall Callable&lt;?&gt; callable,
              @net.bytebuddy.implementation.bind.annotation.This Object inst, @AllArguments Object[] args)
              throws Exception {
          // this is equal to original method, will not cause inner calls to other
          // matching methods get intercepted.
          // Object o = callable.call();
          log.debug(&quot;intercepting {}#{}&quot;, clazz.getName(), method.getName());
          int hashCode = System.identityHashCode(inst);
          if (java.sql.Connection.class.isAssignableFrom(clazz)) {
              ConnectionInfo ci = conns.get(hashCode);
              // TODO fix bug here!!!
              if (ci == null) {
                  log.debug(&quot;connection@{} is not in records anymore, maybe called #{} after close, ignored&quot;, hashCode,
                          method.getName());
              }
              return interceptConnectionMethods(method, hashCode, ci, callable, args);
          } else if (java.sql.Statement.class.isAssignableFrom(clazz)) {
              return interceptStatementMethods(method, hashCode, callable, args);
          } else if (java.sql.PreparedStatement.class.isAssignableFrom(clazz)) {
              return interceptStatementMethods(method, hashCode, callable, args);
          } else if (java.sql.CallableStatement.class.isAssignableFrom(clazz)) {
              return interceptStatementMethods(method, hashCode, callable, args);
          }
          return null;
      }
  
      private static Object interceptConnectionMethods(Method method, int hashCode, ConnectionInfo ci,
              Callable&lt;?&gt; callable, Object[] args) throws Exception {
          Object o = callable.call();
          log.debug(&quot;connection@{} used by {}&quot;, hashCode, method.getName());
          ci.setLastActiveTime(System.currentTimeMillis());
          ci.setLastMethod(method.getName());
          int resultHash = System.identityHashCode(o);
          if (method.getName().equals(&quot;close&quot;)) {
              log.info(&quot;connection@{} released&quot;, hashCode);
              String stackHash = ci.getStackHash();
              Integer scount = counstsByStack.get(stackHash);
              if (scount != null &amp;&amp; scount &gt; 0) {
                  int newscount = scount - 1;
                  log.info(&quot;set connection count to {} by stack hash {}&quot;, newscount, stackHash);
                  if (newscount == 0) {
                      counstsByStack.remove(stackHash);
                      stacks.remove(stackHash);
                  } else {
                      counstsByStack.put(stackHash, newscount);
                  }
              } else {
                  log.error(&quot;connection count by stack hash {} is not supposed to be null or less than zero&quot;, stackHash);
              }
              conns.remove(hashCode);
          } else if (method.getName().equals(&quot;createStatement&quot;)) {
              StatementExecutionInfo stmt = new StatementExecutionInfo();
              stmt.setType(StatementType.NORMAL);
              stmt.setHash(resultHash);
              conns.get(hashCode).setCurrentStatement(stmt);
              log.info(&quot;statement@{} created, type {}, attached to connection@{}&quot;, resultHash, StatementType.NORMAL, hashCode);
          } else if (method.getName().equals(&quot;prepareStatement&quot;)) {
              StatementExecutionInfo stmt = new StatementExecutionInfo();
              stmt.setType(StatementType.PREPARED);
              stmt.setSqlText((String) args[0]);
              stmt.setHash(resultHash);
              conns.get(hashCode).setCurrentStatement(stmt);
              log.info(&quot;statement@{} created, type {}, attached to connection@{}&quot;, resultHash, StatementType.PREPARED, hashCode);
          } else if (method.getName().equals(&quot;prepareCall&quot;)) {
              StatementExecutionInfo stmt = new StatementExecutionInfo();
              stmt.setType(StatementType.CALLABLE);
              stmt.setSqlText((String) args[0]);
              stmt.setHash(resultHash);
              conns.get(hashCode).setCurrentStatement(stmt);
              log.info(&quot;statement@{} created, type {}, attached to connection@{}&quot;, resultHash, StatementType.CALLABLE, hashCode);
          }
          return o;
      }
  
      private static Object interceptStatementMethods(Method method, int hashCode, Callable&lt;?&gt; callable, Object[] args)
              throws Exception {
          log.info(&quot;intercepting statement method {}&quot;, method.getName());
          Object o = null;
          ConnectionInfo containingCI = conns.values().stream().filter(c -&gt; c.getCurrentStatement().getHash() == hashCode)
                  .findFirst().orElse(null);
          if (containingCI == null) {
              log.warn(&quot;unexpected situation happened: statement can't found containing connection!&quot;);
          }
          if (method.getName().equals(&quot;close&quot;)) {
              o = callable.call();
              // TODO statement close method not intercepted ??
              if (containingCI != null) {
                  containingCI.setCurrentStatement(null);
                  log.info(&quot;statement@{} closed, detached from connection@{}&quot;, hashCode, containingCI.getHash());
              }
          }
          // all statement execution method trigger
          else if (method.getName().startsWith(&quot;execute&quot;)) {
              String stack = getCurrentThreadStackTrace();
              String stackHash = StringUtils.md5(stack);
              stacks.put(stackHash, stack);
              log.info(&quot;statement@{} {} started by {}&quot;, hashCode, method.getName(), stackHash);
              long est = System.currentTimeMillis();
              if (containingCI != null) {
                  containingCI.getCurrentStatement().setExecutionStartTime(est);
                  containingCI.getCurrentStatement().setStackHash(stackHash);
                  containingCI.getCurrentStatement().setExecutionCallStack(stack);
                  if (args.length &gt; 0) {
                      containingCI.getCurrentStatement().setSqlText((String) args[0]);
                  }
              }
              Histogram.Timer timer = JDBCMetrics.SQL_EXECUTION_TIME.startTimer();
              try {
                  o = callable.call();
              } finally {
                  timer.observeDuration();
              }
              long eet = System.currentTimeMillis();
              log.info(&quot;statement@{} {} finished, duration is {}&quot;, hashCode, method.getName(), (eet - est));
              if (containingCI != null) {
                  containingCI.getCurrentStatement().setExecutionEndTime(eet);
                  slowStatements.add(containingCI.getCurrentStatement());
              }
          }
          return o;
      }
  
      public static String getCurrentThreadStackTrace() throws IOException {
          StringWriter sw = new StringWriter();
          Throwable t = new Throwable(&quot;&quot;);
          t.printStackTrace(new PrintWriter(sw));
          String stackTrace = sw.toString();
          sw.close();
          return stackTrace;
      }
  
      @Advice.OnMethodExit
      public static void intercept(@net.bytebuddy.asm.Advice.Origin Constructor&lt;?&gt; m, @This Object inst)
              throws Exception {
          log.debug(&quot;----------------- constructor intercept -------------------------&quot;);
          if (java.sql.Connection.class.isAssignableFrom(m.getDeclaringClass())) {
              // some CP library override hashCode method
              int hashCode = System.identityHashCode(inst);
              ConnectionInfo ci = new ConnectionInfo();
              ci.setHash(hashCode);
              ci.setCreationTime(System.currentTimeMillis());
              String stackTrace = getCurrentThreadStackTrace();
              String shash = StringUtils.md5(stackTrace);
              stacks.put(shash, stackTrace);
              // ci.setStack(stackTrace);
              ci.setStackHash(shash);
              log.info(&quot;connection@{} acquired by stack@{}&quot;, hashCode, shash);
              log.debug(stackTrace);
              conns.put(hashCode, ci);
              Integer scount = counstsByStack.get(ci.getStackHash());
              if (scount == null) {
                  counstsByStack.put(ci.getStackHash(), 1);
              } else {
                  counstsByStack.put(ci.getStackHash(), scount + 1);
              }
          }
      }
  
  }
  </code></pre>
  <p>However, this will not work for some cases. Take HikariCP for example:</p>
  <p>HikariCP's implementation has an abstract class <code>ProxyConnection</code>, which has the <code>close</code> method implemented, then a class <code>HikariProxyConnection</code> extends <code>ProxyConnection</code>, which overides some methods, except <code>close</code>. If I setup instrumentation on <code>HikariProxyConnection</code>, the <code>close</code> method will not be intercepted. If I change my code to:</p>
  <pre class="lang-java prettyprint-override"><code>new AgentBuilder.Default()
                  .type(startMatcher.and(isSubTypeOf(java.sql.Connection.class).and(isAbstract()))
                  .transform(constructorTransformer).transform(methodsTransformer).with(listener).installOn(inst);
  </code></pre>
  <p>it will work for HikariCP, but not for other connection pool implementations.</p>
  <p>For my use case, is there a unified way? No matter what connection pool is used, and even with plain JDBC.</p>