JavaAgent源码分析

版权声明:如果觉得文章对你有用,转载不需要联系作者,但请注明出处 https://blog.csdn.net/jinxin70/article/details/85690904

先上测试代码:

业务代码模拟

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

总结

延伸阅读:

猜你喜欢

转载自blog.csdn.net/jinxin70/article/details/85690904