我们都知道在Eclipse中调试代码时,可以直接修改代码,然后继续调试,不需要重新启动它,我一直很好奇这是怎么实现的。找了一段时间后,发现做起来很简单,原理如下:
你可以把目标进程想象成你的被调试程序,而客户进程想象成Eclipse本身。当某些类有变化时,客户进程能探测到这些类的变化,然后动态换掉它们,这样目标进程就可以用上新的类了。
对应的Eclipse工程如下:
其中RunAlways工程对应的就是目标进程,它的main函数里会不停new一个新的User类,然后打印它的名字
1
2
3
4
5
6
7
8
9
10
11
12
|
package
test
;
public
class
Main
{
public
static
void
main
(
String
[
]
args
)
throws
InterruptedException
{
while
(
true
)
{
//替换前,打印出 firstName.lastName
//被替换后,打印lastName.firstName
System
.
out
.
println
(
new
User
(
"firstName"
,
"lastName"
)
.
getName
(
)
)
;
Thread
.
sleep
(
5000
)
;
}
}
}
|
User类的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package
test
;
public
class
User
{
private
String
firstName
;
private
String
lastName
;
public
User
(
String
firstName
,
String
lastName
)
{
this
.
firstName
=
firstName
;
this
.
lastName
=
lastName
;
}
public
String
getName
(
)
{
return
firstName
+
"."
+
lastName
;
}
}
|
先启动它,假设对应的进程号是1234。
接下来是代理jar的编写,这个jar就包含一个类和一个manifest.mf文件。类的内容是
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
package
agent
;
import
java
.
io
.
DataInputStream
;
import
java
.
io
.
File
;
import
java
.
io
.
FileInputStream
;
import
java
.
lang
.
instrument
.
ClassDefinition
;
import
java
.
lang
.
instrument
.
Instrumentation
;
public
class
MyAgent
{
//agentArgs就是VirtualMachine.loadAgent()的第二个参数
public
static
void
agentmain
(
String
agentArgs
,
Instrumentation
inst
)
{
try
{
System
.
out
.
println
(
"args: "
+
agentArgs
)
;
System
.
out
.
println
(
"重新定义 test.User -- 开始"
)
;
//把新的User类文件的内容读出来
File
f
=
new
File
(
agentArgs
)
;
byte
[
]
reporterClassFile
=
new
byte
[
(
int
)
f
.
length
(
)
]
;
DataInputStream
in
=
new
DataInputStream
(
new
FileInputStream
(
f
)
)
;
in
.
readFully
(
reporterClassFile
)
;
in
.
close
(
)
;
//把User类的定义与新的类文件关联起来
ClassDefinition
reporterDef
=
new
ClassDefinition
(
Class
.
forName
(
"test.User"
)
,
reporterClassFile
)
;
//重新定义User类, 妈呀, 太简单了
inst
.
redefineClasses
(
reporterDef
)
;
System
.
out
.
println
(
"重新定义 test.User -- 完成"
)
;
}
catch
(
Exception
e
)
{
System
.
out
.
println
(
e
)
;
e
.
printStackTrace
(
)
;
}
}
}
|
就几句话,看注释就明白了。manifest.mf是放在src/META-INF目录下,内容是
Manifest-Version: 1.0
Agent-Class: agent.MyAgent
Can-Redefine-Classes: true
最后一句是必须的,否则运行起来目标程序会有异常:
1
2
3
4
5
6
7
8
9
|
java
.
lang
.
UnsupportedOperationException
:
redefineClasses
is
not
supported
in
this
environment
at
sun
.
instrument
.
InstrumentationImpl
.
redefineClasses
(
Unknown
Source
)
at
agent
.
MyAgent
.
agentmain
(
MyAgent
.
java
:
24
)
at
sun
.
reflect
.
NativeMethodAccessorImpl
.
invoke0
(
Native
Method
)
at
sun
.
reflect
.
NativeMethodAccessorImpl
.
invoke
(
Unknown
Source
)
at
sun
.
reflect
.
DelegatingMethodAccessorImpl
.
invoke
(
Unknown
Source
)
at
java
.
lang
.
reflect
.
Method
.
invoke
(
Unknown
Source
)
at
sun
.
instrument
.
InstrumentationImpl
.
loadClassAndStartAgent
(
Unknown
Source
)
at
sun
.
instrument
.
InstrumentationImpl
.
loadClassAndCallAgentmain
(
Unknown
Source
)
|
从Eclipse中把这个工程导出为一个jar文件,记得要包含我们的manifest.mf文件。保存在e:/agent.jar(位置随便).
Client工程的Client类是这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package
client
;
import
java
.
lang
.
reflect
.
Field
;
import
com
.
sun
.
tools
.
attach
.
VirtualMachine
;
public
class
Client
{
/**
* @param args
* @throws Exception
*/
public
static
void
main
(
String
[
]
args
)
throws
Exception
{
//注意,是jre的bin目录,不是jdk的bin目录
System
.
setProperty
(
"java.library.path"
,
"C:\\Program Files\\Java\\jdk1.7.0_13\\jre\\bin"
)
;
Field
fieldSysPath
=
ClassLoader
.
class
.
getDeclaredField
(
"sys_paths"
)
;
fieldSysPath
.
setAccessible
(
true
)
;
fieldSysPath
.
set
(
null
,
null
)
;
//目标进程的进程id -- 记得改成正确的数字
VirtualMachine
vm
=
VirtualMachine
.
attach
(
"1234"
)
;
//参数1:代理jar的位置
//参数2, 传递给代理的参数
vm
.
loadAgent
(
"e:/agent.jar"
,
"e:/User.class"
)
;
}
}
|
main函数的前四句是必须的,否则会有这样的异常:
1
2
3
4
|
java
.
util
.
ServiceConfigurationError
:
com
.
sun
.
tools
.
attach
.
spi
.
AttachProvider
:
Provider
sun
.
tools
.
attach
.
WindowsAttachProvider
could
not
be
instantiated
:
java
.
lang
.
UnsatisfiedLinkError
:
no
attach
in
java
.
library
.
path
Exception
in
thread
"main"
com
.
sun
.
tools
.
attach
.
AttachNotSupportedException
:
no
providers
installed
at
com
.
sun
.
tools
.
attach
.
VirtualMachine
.
attach
(
VirtualMachine
.
java
:
208
)
at
client
.
Client
.
main
(
Client
.
java
:
29
)
|
现在就差最后一步了,就是要提供新的User类,它跟旧的User类的差别就是把lastName和firstName调换了位置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package
test
;
public
class
User
{
private
String
firstName
;
private
String
lastName
;
public
User
(
String
firstName
,
String
lastName
)
{
this
.
firstName
=
firstName
;
this
.
lastName
=
lastName
;
}
//打印出来的位置变了
public
String
getName
(
)
{
return
lastName
+
"."
+
firstName
;
}
}
|
直接把这个类对应的class文件复制到e:/User.class (简单起见,你可以随便放在什么地方)。写了半天,终于可以看看成果了,直接运行Client类。然后看看目标进程的输出:
firstName.lastName
args: e:/User.class
重新定义 test.User — 开始
重新定义 test.User — 完成
lastName.firstName
lastName.firstName
妈呀,User类真的被我们改了。有点美中不足的是这个类只能被改一次,能不能像Eclise那样每次e:/User.class有变化都重新加载呢,这对整天写Java的我们来说,简直难度为0,直接启动一个线程,不停看那个文件,只要修改时间有变化,就重新加载它:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
package
agent
;
import
java
.
io
.
DataInputStream
;
import
java
.
io
.
File
;
import
java
.
io
.
FileInputStream
;
import
java
.
lang
.
instrument
.
ClassDefinition
;
import
java
.
lang
.
instrument
.
Instrumentation
;
public
class
ClassFileWatcher
extends
Thread
{
private
File
classFile
;
private
long
lastModified
=
0
;
private
Instrumentation
inst
;
public
ClassFileWatcher
(
Instrumentation
inst
,
File
classFile
)
{
this
.
classFile
=
classFile
;
this
.
inst
=
inst
;
lastModified
=
classFile
.
lastModified
(
)
;
}
@Override
public
void
run
(
)
{
while
(
true
)
{
//如果class文件有变化,则重新加载
if
(
lastModified
!=
classFile
.
lastModified
(
)
)
{
lastModified
=
classFile
.
lastModified
(
)
;
System
.
out
.
println
(
"重新定义 test.User -- 开始"
)
;
byte
[
]
reporterClassFile
=
new
byte
[
(
int
)
classFile
.
length
(
)
]
;
DataInputStream
in
;
try
{
in
=
new
DataInputStream
(
new
FileInputStream
(
classFile
)
)
;
in
.
readFully
(
reporterClassFile
)
;
in
.
close
(
)
;
// 把User类的定义与新的类文件关联起来
ClassDefinition
reporterDef
=
new
ClassDefinition
(
Class
.
forName
(
"test.User"
)
,
reporterClassFile
)
;
// 重新定义User类, 妈呀, 太简单了
inst
.
redefineClasses
(
reporterDef
)
;
System
.
out
.
println
(
"重新定义test.User -- 完成"
)
;
}
catch
(
Exception
e
)
{
e
.
printStackTrace
(
)
;
}
}
try
{
Thread
.
sleep
(
3000
)
;
}
catch
(
InterruptedException
e
)
{
e
.
printStackTrace
(
)
;
}
}
}
}
|
代理类改成这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package
agent
;
import
java
.
io
.
File
;
import
java
.
lang
.
instrument
.
Instrumentation
;
public
class
MyAgent
{
//agentArgs就是VirtualMachine.loadAgent()的第二个参数
public
static
void
agentmain
(
String
agentArgs
,
Instrumentation
inst
)
{
try
{
System
.
out
.
println
(
"args: "
+
agentArgs
)
;
//把新的User类文件的内容读出来
File
f
=
new
File
(
agentArgs
)
;
ClassFileWatcher
watcher
=
new
ClassFileWatcher
(
inst
,
f
)
;
watcher
.
start
(
)
;
}
catch
(
Exception
e
)
{
System
.
out
.
println
(
e
)
;
e
.
printStackTrace
(
)
;
}
}
}
|
现在重新把整个过程再跑一遍,然后用新的User.class覆盖e:/User.class,每次覆盖后,你就会发现目标进程已经用上了新的版本,酷啊。