简体   繁体   English

如何拦截继承的方法

[英]How to intercept inherited method

I want to build a Java agent with Byte Buddy, which intercepts JDBC connection creation and closing event.我想用 Byte Buddy 构建一个 Java 代理,它拦截 JDBC 连接创建和关闭事件。 I want to support data sources such as HikariCP, DBCP, etc, also plain JDBC with RDBMS drivers.我想支持诸如 HikariCP、DBCP 等数据源,以及带有 RDBMS 驱动程序的普通 JDBC。

My code:我的代码:

Instrumention setup仪器设置

new AgentBuilder.Default()
                .type(startMatcher.and(isSubTypeOf(java.sql.Connection.class).and(not(isAbstract())))
                .transform(constructorTransformer).transform(methodsTransformer).with(listener).installOn(inst);

Intercepting 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<Integer, ConnectionInfo> conns = new HashMap<Integer, ConnectionInfo>();
    
    public static Map<String, String> stacks = new HashMap<String, String>();

    public static Map<String, Integer> counstsByStack = new HashMap<String, Integer>();
    
    public static TopN<StatementExecutionInfo> slowStatements = new TopN<StatementExecutionInfo>(10, new Comparator<StatementExecutionInfo>() {
        @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<ConnectionInfo> data = new ArrayList<ConnectionInfo>();
        for (ConnectionInfo ci : conns.values()) {
            ConnectionInfo copy = new ConnectionInfo();
            copy.setHash(ci.getHash());
            copy.setStackHash(ci.getStackHash() + "(" + counstsByStack.get(ci.getStackHash()) + ")");
            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<?> clazz, @Origin Method method, @SuperCall Callable<?> 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("intercepting {}#{}", 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("connection@{} is not in records anymore, maybe called #{} after close, ignored", 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<?> callable, Object[] args) throws Exception {
        Object o = callable.call();
        log.debug("connection@{} used by {}", hashCode, method.getName());
        ci.setLastActiveTime(System.currentTimeMillis());
        ci.setLastMethod(method.getName());
        int resultHash = System.identityHashCode(o);
        if (method.getName().equals("close")) {
            log.info("connection@{} released", hashCode);
            String stackHash = ci.getStackHash();
            Integer scount = counstsByStack.get(stackHash);
            if (scount != null && scount > 0) {
                int newscount = scount - 1;
                log.info("set connection count to {} by stack hash {}", newscount, stackHash);
                if (newscount == 0) {
                    counstsByStack.remove(stackHash);
                    stacks.remove(stackHash);
                } else {
                    counstsByStack.put(stackHash, newscount);
                }
            } else {
                log.error("connection count by stack hash {} is not supposed to be null or less than zero", stackHash);
            }
            conns.remove(hashCode);
        } else if (method.getName().equals("createStatement")) {
            StatementExecutionInfo stmt = new StatementExecutionInfo();
            stmt.setType(StatementType.NORMAL);
            stmt.setHash(resultHash);
            conns.get(hashCode).setCurrentStatement(stmt);
            log.info("statement@{} created, type {}, attached to connection@{}", resultHash, StatementType.NORMAL, hashCode);
        } else if (method.getName().equals("prepareStatement")) {
            StatementExecutionInfo stmt = new StatementExecutionInfo();
            stmt.setType(StatementType.PREPARED);
            stmt.setSqlText((String) args[0]);
            stmt.setHash(resultHash);
            conns.get(hashCode).setCurrentStatement(stmt);
            log.info("statement@{} created, type {}, attached to connection@{}", resultHash, StatementType.PREPARED, hashCode);
        } else if (method.getName().equals("prepareCall")) {
            StatementExecutionInfo stmt = new StatementExecutionInfo();
            stmt.setType(StatementType.CALLABLE);
            stmt.setSqlText((String) args[0]);
            stmt.setHash(resultHash);
            conns.get(hashCode).setCurrentStatement(stmt);
            log.info("statement@{} created, type {}, attached to connection@{}", resultHash, StatementType.CALLABLE, hashCode);
        }
        return o;
    }

    private static Object interceptStatementMethods(Method method, int hashCode, Callable<?> callable, Object[] args)
            throws Exception {
        log.info("intercepting statement method {}", method.getName());
        Object o = null;
        ConnectionInfo containingCI = conns.values().stream().filter(c -> c.getCurrentStatement().getHash() == hashCode)
                .findFirst().orElse(null);
        if (containingCI == null) {
            log.warn("unexpected situation happened: statement can't found containing connection!");
        }
        if (method.getName().equals("close")) {
            o = callable.call();
            // TODO statement close method not intercepted ??
            if (containingCI != null) {
                containingCI.setCurrentStatement(null);
                log.info("statement@{} closed, detached from connection@{}", hashCode, containingCI.getHash());
            }
        }
        // all statement execution method trigger
        else if (method.getName().startsWith("execute")) {
            String stack = getCurrentThreadStackTrace();
            String stackHash = StringUtils.md5(stack);
            stacks.put(stackHash, stack);
            log.info("statement@{} {} started by {}", 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 > 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("statement@{} {} finished, duration is {}", 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("");
        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<?> m, @This Object inst)
            throws Exception {
        log.debug("----------------- constructor intercept -------------------------");
        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("connection@{} acquired by stack@{}", 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);
            }
        }
    }

}

However, this will not work for some cases.但是,这在某些情况下不起作用。 Take HikariCP for example:以 HikariCP 为例:

HikariCP's implementation has an abstract class ProxyConnection , which has the close method implemented, then a class HikariProxyConnection extends ProxyConnection , which overides some methods, except close . HikariCP 的实现有一个抽象的 class ProxyConnection ,它实现了close方法,然后是一个 class HikariProxyConnection扩展了ProxyConnection ,它覆盖了一些方法,除了close If I setup instrumentation on HikariProxyConnection , the close method will not be intercepted.如果我在HikariProxyConnection上设置检测,则不会拦截close方法。 If I change my code to:如果我将代码更改为:

new AgentBuilder.Default()
                .type(startMatcher.and(isSubTypeOf(java.sql.Connection.class).and(isAbstract()))
                .transform(constructorTransformer).transform(methodsTransformer).with(listener).installOn(inst);

it will work for HikariCP, but not for other connection pool implementations.它适用于 HikariCP,但不适用于其他连接池实现。

For my use case, is there a unified way?对于我的用例,有没有统一的方法? No matter what connection pool is used, and even with plain JDBC.无论使用什么连接池,即使是普通的 JDBC。

Your matcher:你的匹配器:

isSubTypeOf(java.sql.Connection.class).and(not(isAbstract()) 

explicitly excludes abstract classes.明确排除抽象类。 You would need to instrument all methods to achieve what you wanted if you are using Advice .如果您使用Advice ,您将需要检测所有方法以实现您想要的。

Also, it seems like you are blending concepts:此外,您似乎正在混合概念:

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;

The latter annotations belong to MethodDelegation , not to Advice .后面的注释属于MethodDelegation ,而不是Advice Refer to the javadoc to see how advice should be applied.请参阅 javadoc 以了解如何应用建议。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM