如何检测一个线程已经开始使用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对象中的方法.然后我们继续检查它是否是 start
和 join
.
这两个特定方法之一
当这两种情况之一发生时,我们使用 javassist highlevel API 进行代码替换。替换很容易在代码中发现,实际提供的代码可能有点棘手,所以让我们拆分其中一行,让我们以检测线程开始的行为例:
{ System.out.println(\"Detected thread starting with id: \" + ((Thread)[=17=]).getId()); $proceed($$); } "
- 首先所有代码都在大括号内,否则javassist不会接受
- 然后你有一个引用 $0 的 System.out。 $0 是一个特殊的参数,可以在 javassist 代码操作中使用,表示方法调用的目标对象,在这种情况下我们肯定知道它将是一个线程。
- $proceed($$) 如果你不熟悉 javassist,这可能是最棘手的指令,因为它都是 javassist 的特殊指令糖,根本没有 java。 $proceed 是您必须引用您正在处理的实际方法调用以及 $$ 对传递给方法调用的完整参数列表的引用的方式。在这种特殊情况下,start 和 join 都将此列表为空,但我认为最好保留此信息。
您可以在 Javassist 教程的 4.2 Altering a Method Body 部分(搜索 MethodCall 小节,抱歉,该小节没有锚点)
中阅读有关此特殊运算符的更多信息
最后,在所有这些 功夫 之后,我们将 ctClass 的字节码写入 class 文件夹(因此它会覆盖现有的 GuinePig.class 文件)并且当我们执行它时...voila,输出现在就是我们想要的:-)
只是一个最后的警告,请记住这个注入器非常简单并且不会检查 class 是否已经被注入所以你可以结束多次注射。
我必须在每个线程的开头和结尾检测任何给定代码(不直接更改给定代码)。简单地说,我如何在任何线程的入口和出口点打印一些东西。
我如何使用 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对象中的方法.然后我们继续检查它是否是 start
和 join
.
当这两种情况之一发生时,我们使用 javassist highlevel API 进行代码替换。替换很容易在代码中发现,实际提供的代码可能有点棘手,所以让我们拆分其中一行,让我们以检测线程开始的行为例:
{ System.out.println(\"Detected thread starting with id: \" + ((Thread)[=17=]).getId()); $proceed($$); } "
- 首先所有代码都在大括号内,否则javassist不会接受
- 然后你有一个引用 $0 的 System.out。 $0 是一个特殊的参数,可以在 javassist 代码操作中使用,表示方法调用的目标对象,在这种情况下我们肯定知道它将是一个线程。
- $proceed($$) 如果你不熟悉 javassist,这可能是最棘手的指令,因为它都是 javassist 的特殊指令糖,根本没有 java。 $proceed 是您必须引用您正在处理的实际方法调用以及 $$ 对传递给方法调用的完整参数列表的引用的方式。在这种特殊情况下,start 和 join 都将此列表为空,但我认为最好保留此信息。
您可以在 Javassist 教程的 4.2 Altering a Method Body 部分(搜索 MethodCall 小节,抱歉,该小节没有锚点)
中阅读有关此特殊运算符的更多信息最后,在所有这些 功夫 之后,我们将 ctClass 的字节码写入 class 文件夹(因此它会覆盖现有的 GuinePig.class 文件)并且当我们执行它时...voila,输出现在就是我们想要的:-)
只是一个最后的警告,请记住这个注入器非常简单并且不会检查 class 是否已经被注入所以你可以结束多次注射。