先上测试代码:
业务代码模拟
AccountMain.java
package com.rong.kim.agenttest;
import com.rong.kim.common.Lion;
public class AccountMain {
public static void main(String[] args) throws InterruptedException {
for (;;) {
new Lion().runLion();
Thread.sleep(5000);
}
}
}
AccountMain模拟业务代码,main方法中有一个死循环,实例化Lion并运行runLion方法,然后休眠5秒。
即每5秒产生一个新的Lion实例,并运行这个实例的runLion方法。
Lion.java
package com.rong.kim.common;
/**
* 模拟一个业务模型类
*/
public class Lion {
public void runLion() throws InterruptedException {
System.out.println("Lion is going to run........");
}
}
先运行AccountMain,假装我们的业务正在运行。
此时,使用jps -l 能够看到这个业务线程运行的端口号
agent代码模拟
AgentMainTraceAgent.java
package com.umbrella.robot.agent;
import com.rong.kim.common.Lion;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
public class AgentMainTraceAgent {
public static void agentmain(String agentArgs, Instrumentation inst)
throws UnmodifiableClassException {
System.out.println("Agent Main called");
System.out.println("agentArgs : " + agentArgs);
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
System.out.println("agentmain load Class :" + className);
return classfileBuffer;
}
}, true);
inst.retransformClasses(Lion.class);
}
}
META-INF/MANIFEST.MF
Manifest-Version: 1.0
Agent-Class: com.umbrella.robot.agent.AgentMainTraceAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
AgentMainTraceAgent中使用agentmain方法,方法签名第一个参数是传入到agent中的参数;看一下Instrumentation。
Instrumentation是java.lang.instrument包下的一个重要的接口,看一下接口方法:
Instrumentation接口设计初衷是为了收集Java程序运行时的数据,用于监控运行程序状态,记录日志,分析代码用的。
提供了两种agent使用方式:
1、以-javaagent:
jarpath[=
options] 的形式启动JVM时,在这种情况下,Instrumentation实例将传递给代理类的premain方法。
2、在JVM启动后的某个时间,提供启动代理的机制。在这种情况下,Instrumentation实例将传递给代理程序代码的agentmain方法。
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
实现类InstrumentationImpl中的方法实现如下:
public synchronized void addTransformer(ClassFileTransformer var1, boolean var2) {
if (var1 == null) {
throw new NullPointerException("null passed as 'transformer' in addTransformer");
} else {
if (var2) {
if (!this.isRetransformClassesSupported()) {
throw new UnsupportedOperationException("adding retransformable transformers is not supported in this environment");
}
if (this.mRetransfomableTransformerManager == null) {
this.mRetransfomableTransformerManager = new TransformerManager(true);
}
this.mRetransfomableTransformerManager.addTransformer(var1);
if (this.mRetransfomableTransformerManager.getTransformerCount() == 1) {
this.setHasRetransformableTransformers(this.mNativeAgent, true);
}
} else {
this.mTransformerManager.addTransformer(var1);
}
}
}
看着一行this.mRetransfomableTransformerManager.addTransformer(var1);
TransformerManager.addTransformer
public synchronized void addTransformer(ClassFileTransformer var1) {
TransformerManager.TransformerInfo[] var2 = this.mTransformerList;
TransformerManager.TransformerInfo[] var3 = new TransformerManager.TransformerInfo[var2.length + 1];
System.arraycopy(var2, 0, var3, 0, var2.length);
var3[var2.length] = new TransformerManager.TransformerInfo(var1);
this.mTransformerList = var3;
}
从这段代码知道,转换器ClassFileTransformer的实现是存储在TransformerManager内的TransformerInfo数组中的,数组初识长度为0,每添加一个,数组长度扩容为原来的长度+1,将原数组内容拷贝到新数组中。
触发程序:
package com.umbrella.robot.agentmain;
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class JVMTIThread {
public static void main(String[] args)
throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
if (vmd.displayName().endsWith("AccountMain")) {
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
try {
virtualMachine.loadAgent("E:\\public-files\\javaagentparent\\agentmain\\target\\my-agent.jar", "cxs");
System.out.println("ok");
}catch (Exception e){
e.printStackTrace();
}
finally {
virtualMachine.detach();
}
}
}
}
}
我们的业务代码AccountMain一直在运行,如果想要注入agent,要调用VirtualMachine.attach(String var0)访问到远程JVM线程,传入一个线程访问端口。
然后加载本地agent程序:virtualMachine.loadAgent,参数为agent.jar位置。
代码中的my-agent.jar为AgentMainTraceAgent所在项目打成的jar包。
由于VirtualMachine.attach(vmd.id())方法只需要你传入JVM线程端口,所以你也可以使用jps -l查看线程端口,手动填上。
VirtualMachine.attach(vmd.id())
先追下attach方法:
public static VirtualMachine attach(String var0) throws AttachNotSupportedException, IOException {
if (var0 == null) {
throw new NullPointerException("id cannot be null");
} else {
List var1 = AttachProvider.providers();
if (var1.size() == 0) {
throw new AttachNotSupportedException("no providers installed");
} else {
AttachNotSupportedException var2 = null;
Iterator var3 = var1.iterator();
while(var3.hasNext()) {
AttachProvider var4 = (AttachProvider)var3.next();
try {
return var4.attachVirtualMachine(var0);
} catch (AttachNotSupportedException var6) {
var2 = var6;
}
}
throw var2;
}
}
}
前两行快速试错,判断线程id不能为空,否则报NPE。紧接着看这行:
List var1 = AttachProvider.providers();
获取AttachProvider
代码追进去:
public static List<AttachProvider> providers() {
Object var0 = lock;
synchronized(lock) {
if (providers == null) {
providers = new ArrayList();
ServiceLoader var1 = ServiceLoader.load(AttachProvider.class, AttachProvider.class.getClassLoader());
Iterator var2 = var1.iterator();
while(var2.hasNext()) {
try {
providers.add(var2.next());
} catch (Throwable var6) {
if (var6 instanceof ThreadDeath) {
ThreadDeath var4 = (ThreadDeath)var6;
throw var4;
}
System.err.println(var6);
}
}
}
return Collections.unmodifiableList(providers);
}
}
这里面会初始化AttachProvider,并返回一个AttachProvider列表。
ServiceLoader原理
看看下面通过ServiceLoader怎么加载AttachProvider的
ServiceLoader var1 = ServiceLoader.load(AttachProvider.class, AttachProvider.class.getClassLoader());
public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
继续跟进ServiceLoader
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
看到构造方法里最后调用reload方法,reload做了两件事,首先将providers清空,然后实例化一个迭代器LazyIterator。
迭代器初始化传入两个参数,第一个是要注册的服务,这里是AttachProvider.class,第一个是类加载器,没有传使用系统类加载器,这里是AttachProvider.class.getClassLoader()。
迭代器中有一个主要的方法hasNextService
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
hasNextService方法中,使用类加载器加载"META-INF/services/"目录下的配置文件,然后调用parse方法解析配置。
看一下,com.sun.tools下的META-INF/services/目录
看看com.sun.tools.attach.spi.AttachProvider文件内容,可以看到有不同平台操作系统的实现,我的是windows,会调用windos实现sun.tools.attach.WindowsAttachProvider。
所以,代码看到这,就知道ServiceLoader.load方法最终加载的是sun.tools.attach.WindowsAttachProvider。
具体的:
1、每次调用ServiceLoader内部迭代器的hasNext()方法,都会调用hasNextService去加载配置文件,解析配置文件。
2、紧接着调用ServiceLoader内部迭代器的next()方法,next()方法内部调用nextService()方法,使用Class.forName加载解析的类。
然后放入linkedhashmap中缓存起来,键是服务的全限定名字符串,比如sun.tools.attach.WindowsAttachProvider,value是这个服务的实例。
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
再回到VirtualMachine.attach方法:
public static VirtualMachine attach(String var0) throws AttachNotSupportedException, IOException {
if (var0 == null) {
throw new NullPointerException("id cannot be null");
} else {
List var1 = AttachProvider.providers();
if (var1.size() == 0) {
throw new AttachNotSupportedException("no providers installed");
} else {
AttachNotSupportedException var2 = null;
Iterator var3 = var1.iterator();
while(var3.hasNext()) {
AttachProvider var4 = (AttachProvider)var3.next();
try {
return var4.attachVirtualMachine(var0);
} catch (AttachNotSupportedException var6) {
var2 = var6;
}
}
throw var2;
}
}
}
通过上面对List var1 = AttachProvider.providers();这一句的分析,知道其内部是通过ServiceLoader.load方法加载META-INF/services/目录下的对应配置,windows电脑加载的就是sun.tools.attach.WindowsAttachProvider。
下面的步骤就是使用迭代器遍历AttachProvider.providers()返回的List。
while(var3.hasNext()) {
AttachProvider var4 = (AttachProvider)var3.next();
try {
return var4.attachVirtualMachine(var0);
} catch (AttachNotSupportedException var6) {
var2 = var6;
}
}
追这行return var4.attachVirtualMachine(var0);
public VirtualMachine attachVirtualMachine(String var1) throws AttachNotSupportedException, IOException {
this.checkAttachPermission();
this.testAttachable(var1);
return new WindowsVirtualMachine(this, var1);
}
继续追return new WindowsVirtualMachine(this, var1);这行,其他先不用管。
WindowsVirtualMachine(AttachProvider var1, String var2) throws AttachNotSupportedException, IOException {
super(var1, var2);
int var3;
try {
var3 = Integer.parseInt(var2);
} catch (NumberFormatException var6) {
throw new AttachNotSupportedException("Invalid process identifier");
}
this.hProcess = openProcess(var3);
try {
enqueue(this.hProcess, stub, (String)null, (String)null);
} catch (IOException var5) {
throw new AttachNotSupportedException(var5.getMessage());
}
}
这里的openProcess方法是调用native方法,我的windows上调用的是D:\Program Files\Java\jdk1.8.0_192\jre\bin\attach.dll。
继续单点调试,发现在attach.dll中找方法名为Java_sun_tools_attach_WindowsVirtualMachine_openProcess的偏移量,不等于0便返回这个偏移量。
ClassLoader类中的findNative方法,可以找到JVM源码中的一些native方法调用名,这样可以关联着JVM源码看底层的C++源码到底做了啥
openProcess方法调用结束返回一个long型数字,然后将其放入一个队列,这个enqueue方法也是native的。
try {
enqueue(this.hProcess, stub, (String)null, (String)null);
} catch (IOException var5) {
throw new AttachNotSupportedException(var5.getMessage());
}
再次进入ClassLoader.findNative方法,可以发现这次是在attach.dll中找Java_sun_tools_attach_WindowsVirtualMachine_enqueue的方法实现。
这次尝试性的找了一下这个方法Java_sun_tools_attach_WindowsVirtualMachine_enqueue,截图如下,可以清楚地定位到E:\public-files\openjdk\jdk\src\windows\native\sun\tools\attach\WindowsVirtualMachine.c这个文件中的代码
小结
VirtualMachine.attach,通过SPI机制,使用不同平台的实现,和远程JVM通信。通信机制还不清楚?
virtualMachine.loadAgent()
实现也是平台相关的,代码就不跟了,
windows实现在WindowsVirtualMachine的InputStream execute(String var1, Object... var2)方法中
1、windows平台的实现使用到了管道,首先创建一个Pipe
管道仅用于不同线程间信息交换,不同JVM进程间的连接已经调用上面的Java_sun_tools_attach_WindowsVirtualMachine_openProcess方法建立了。
long var5 = createPipe(var4);
底层调用WindowsVirtualMachine.c的Java_sun_tools_attach_WindowsVirtualMachine_createPipe方法
/*
* Class: sun_tools_attach_WindowsVirtualMachine
* Method: createPipe
* Signature: (Ljava/lang/String;)J
*/
JNIEXPORT jlong JNICALL Java_sun_tools_attach_WindowsVirtualMachine_createPipe
(JNIEnv *env, jclass cls, jstring pipename)
{
HANDLE hPipe;
char name[MAX_PIPE_NAME_LENGTH];
SECURITY_ATTRIBUTES sa;
LPSECURITY_ATTRIBUTES lpSA = NULL;
// Custom Security Descriptor is required here to "get" Medium Integrity Level.
// In order to allow Medium Integrity Level clients to open
// and use a NamedPipe created by an High Integrity Level process.
TCHAR *szSD = TEXT("D:") // Discretionary ACL
TEXT("(A;OICI;GRGW;;;WD)") // Allow read/write to Everybody
TEXT("(A;OICI;GA;;;SY)") // Allow full control to System
TEXT("(A;OICI;GA;;;BA)"); // Allow full control to Administrators
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = FALSE;
sa.lpSecurityDescriptor = NULL;
if (ConvertStringSecurityDescriptorToSecurityDescriptor
(szSD, SDDL_REVISION_1, &(sa.lpSecurityDescriptor), NULL)) {
lpSA = &sa;
}
jstring_to_cstring(env, pipename, name, MAX_PIPE_NAME_LENGTH);
hPipe = CreateNamedPipe(
name, // pipe name
PIPE_ACCESS_INBOUND, // read access
PIPE_TYPE_BYTE | // byte mode
PIPE_READMODE_BYTE |
PIPE_WAIT, // blocking mode
1, // max. instances
128, // output buffer size
8192, // input buffer size
NMPWAIT_USE_DEFAULT_WAIT, // client time-out
lpSA); // security attributes
LocalFree(sa.lpSecurityDescriptor);
if (hPipe == INVALID_HANDLE_VALUE) {
JNU_ThrowIOExceptionWithLastError(env, "CreateNamedPipe failed");
}
return (jlong)hPipe;
}
2、接着调用enqueue(this.hProcess, stub, var1, var4, var2);
这个是native方法,会触发业务线程的loadClassAndCallAgentmain的调用,然后调用我们对ClassFileTransformer的实现
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
System.out.println("agentmain load Class :" + className);
return classfileBuffer;
}
}, true);
inst.retransformClasses(Lion.class);
然后连接管道拿到远程JVM线程返回的信息,包装在PipedInputStream中:
connectPipe(var5);
WindowsVirtualMachine.PipedInputStream var7 = new WindowsVirtualMachine.PipedInputStream(var5);
int var8 = this.readInt(var7);
WindowsVirtualMachine.java中成功读取到的流中的数据一般不为0,然后直接返回流。
if (var8 != 0) {
String var9 = this.readErrorMessage(var7);
if (var1.equals("load")) {
throw new AgentLoadException("Failed to load agent library");
} else if (var9 == null) {
throw new AttachOperationFailedException("Command failed in target VM");
} else {
throw new AttachOperationFailedException(var9);
}
} else {
return var7;
}
小结:
管道只是用于信息交换,具体的远程调用逻辑在enqueue(this.hProcess, stub, var1, var4, var2)中。
enqueue(this.hProcess, stub, var1, var4, var2)的分析另写文章,这里先把agentmain主要逻辑跟完。
virtualMachine.detach()
public void detach() throws IOException {
synchronized(this) {
if (this.hProcess != -1L) {
closeProcess(this.hProcess);
this.hProcess = -1L;
}
}
}
hProcess是调用openProcess建立连接时返回的,连接未关闭不为-1,;
调用native方法cloassProcess关闭连接,传入hProcess参数;
充值hProcess值为-1。
/*
* Class: sun_tools_attach_WindowsVirtualMachine
* Method: closeProcess
* Signature: (J)V
*/
JNIEXPORT void JNICALL Java_sun_tools_attach_WindowsVirtualMachine_closeProcess
(JNIEnv *env, jclass cls, jlong hProcess)
{
CloseHandle((HANDLE)hProcess);
}
Java_sun_tools_attach_WindowsVirtualMachine_closeProcess方法会执行关闭远程进程 的方法CloseHandle
总结
延伸阅读: