如何检测一个线程已经开始使用javassist?

How to detect that a thread has started using javassist?

我必须在每个线程的开头和结尾检测任何给定代码(不直接更改给定代码)。简单地说,我如何在任何线程的入口和出口点打印一些东西。

我如何使用 javassist 做到这一点?

简答

您可以通过创建一个与线程对象的开始和连接相匹配的 ExprEditor and use it to modify MethodCall 来做到这一点。

(非常)长答案(带代码)

在我们开始之前,我要说的是,您不应该被冗长的 post 吓到,其中大部分只是代码,一旦您将其分解,就会很容易理解!

那我们开始忙吧...

假设您有以下虚拟代码:

public class GuineaPig {

 public void test() throws InterruptedException {
    Thread t = new Thread(new Runnable() {

        @Override
        public void run() {
            for (int i = 0; i < 10; i++)
                System.out.println(i);
        }
    });

    t.start();
    System.out.println("Sleeping 10 seconds");
    Thread.sleep(10 * 1000);
    System.out.println("Done joining thread");
    t.join();

 }
}

当你运行这段代码做

new GuineaPig().test();

你得到这样的输出(睡眠 system.out 可能会出现在计数的中间,因为它 运行 在主线程中):

 Sleeping 10 seconds
 0
 1
 2
 3
 4
 5 
 6
 7
 8
 9
 Done joining thread

我们的 objective 是创建一个代码注入器,它将对以下内容进行输出更改:

 Detected thread starting with id: 10
 Sleeping 10 seconds
 0
 1
 2
 3
 4
 5 
 6
 7
 8
 9
 Done joining thread
 Detected thread joining with id: 10

我们能做的有点受限,但我们能够注入代码并访问线程引用。希望这对您来说足够了,如果还不够,我们仍然可以尝试多讨论一下。

考虑到所有这些想法,我们创建了以下注入器:

 ClassPool classPool = ClassPool.getDefault();
 CtClass guineaPigCtClass = classPool.get(GuineaPig.class.getName());

    guineaPigCtClass.instrument(new ExprEditor() {

        @Override
        public void edit(MethodCall m) throws CannotCompileException {
            CtMethod method = null;
            try {
                method = m.getMethod();
            } catch (NotFoundException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            String classname = method.getDeclaringClass().getName();
            String methodName = method.getName();
            if (classname.equals(Thread.class.getName())
                    && methodName.equals("start")) {
                m.replace("{ System.out.println(\"Detected thread starting with id: \" + ((Thread)[=14=]).getId()); $proceed($$); } ");
            } else if (classname.equals(Thread.class.getName())
                    && methodName.equals("join")) {
                m.replace("{ System.out.println(\"Detected thread joining with id: \" + ((Thread)[=14=]).getId());  $proceed($$); } ");
            }

        }
    });

    guineaPigCtClass
            .writeFile("<Your root directory with the class files>");
}

那么在这段漂亮的小代码中发生了什么?我们使用 ExprEdit 检测我们的 GuineaPig class(不对它造成任何伤害!)并拦截所有方法调用。

当我们拦截一个方法调用时,我们首先检查方法的声明class是否是一个线程class,如果是这样就意味着我们正在调用一个Thread对象中的方法.然后我们继续检查它是否是 startjoin.

这两个特定方法之一

当这两种情况之一发生时,我们使用 javassist highlevel API 进行代码替换。替换很容易在代码中发现,实际提供的代码可能有点棘手,所以让我们拆分其中一行,让我们以检测线程开始的行为例:

{ System.out.println(\"Detected thread starting with id: \" + ((Thread)[=17=]).getId()); $proceed($$); } "

  1. 首先所有代码都在大括号内,否则javassist不会接受
  2. 然后你有一个引用 $0 的 System.out。 $0 是一个特殊的参数,可以在 javassist 代码操作中使用,表示方法调用的目标对象,在这种情况下我们肯定知道它将是一个线程。
  3. $proceed($$) 如果你不熟悉 javassist,这可能是最棘手的指令,因为它都是 javassist 的特殊指令糖,根本没有 java。 $proceed 是您必须引用您正在处理的实际方法调用以及 $$ 对传递给方法调用的完整参数列表的引用的方式。在这种特殊情况下,start 和 join 都将此列表为空,但我认为最好保留此信息。

您可以在 Javassist 教程的 4.2 Altering a Method Body 部分(搜索 MethodCall 小节,抱歉,该小节没有锚点)

中阅读有关此特殊运算符的更多信息

最后,在所有这些 功夫 之后,我们将 ctClass 的字节码写入 class 文件夹(因此它会覆盖现有的 GuinePig.class 文件)并且当我们执行它时...voila,输出现在就是我们想要的:-)

只是一个最后的警告,请记住这个注入器非常简单并且不会检查 class 是否已经被注入所以你可以结束多次注射。