如何检测线程是否已开始使用javassist?

时间:2015-07-07 04:29:24

标签: javassist

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

如何使用javassist做到这一点?

1 个答案:

答案 0 :(得分:1)

简答

您可以通过创建ExprEditor并使用它来修改与线程对象的启动和连接匹配的MethodCall来完成此操作。

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

在我们开始之前,让我说你不应该被长篇文章吓倒,大多数只是代码,一旦你打破了它,它很容易理解!

让我们忙碌然后......

想象一下,你有以下虚拟代码:

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

我们的目标是创建一个代码注入器,使输出更改为以下内容:

 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)$0).getId()); $proceed($$); } ");
            } else if (classname.equals(Thread.class.getName())
                    && methodName.equals("join")) {
                m.replace("{ System.out.println(\"Detected thread joining with id: \" + ((Thread)$0).getId());  $proceed($$); } ");
            }

        }
    });

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

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

当我们拦截一个方法调用时,我们首先检查方法的声明类是否是一个Thread类,如果是这样的话,那就意味着我们在Thread对象中调用一个方法。然后,我们会检查它是否是startjoin两种特定方法中的一种。

当这两个案例中的一个发生时,我们使用javassist高级API来进行代码替换。替换很容易在代码中找到,提供的实际代码可能有点棘手,所以让我们拆分其中一行,让我们以例如检测Thread开始的行为例:

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

  1. 首先所有代码都在大括号内,否则javassist不会接受它
  2. 然后你有一个引用$ 0的System.out 。 $ 0是一个特殊参数,可以在javassist代码操作中使用,并表示方法调用的目标对象,在这种情况下,我们确定它将是一个Thread。
  3. $ proceed($$)如果您不熟悉javassist,这可能是最棘手的指令,因为它是所有javassist特殊糖而根本没有java。 $ proceed是您必须引用正在处理的实际方法调用以及对传递给方法调用的完整参数列表的$$引用的方式。在这种特殊情况下,启动和加入都会将此列表清空,但我认为保留此信息会更好。
  4. 您可以在Javassist教程的第4.2 Altering a Method Body部分中阅读有关此特殊操作符的更多信息(搜索MethodCall子部分,对不起,那个子部分没有锚点)

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

    只是最后一次警告,请记住,此注射器非常简单,不会检查是否已经注射了该类,因此最终可能会进行多次注射。