Javaagent

Mars 2020年07月19日 33次浏览

前言

不了解agent技术的请参考java插桩、探针技术

构建agent jar包

构建agent项目

  • agent项目存在两种形式,分别是启动前调用premian方法,一种是启动成功之后通过attach的方式加载agent包调用agentmain方法。
    • 当Java 虚拟机启动时,在执行 main 函数之前,JVM 会先运行-javaagent所指定 jar 包内 Premain-Class 这个类的 premain 方法 。
    • premain方法存在两种形式
      会优先选择执行两个参数的方法
    	public static void    premain(String agentOps,Instrumentation inst){}
    	public static void    premain(String agentOps){}	
    
    • 此外,java还提供了在jvm启动成功之后加载agent的方法,即attach的方式加载。当加载成功之后会执行jar包内Agent-Class指定类的agentmain方法。jvm也是会优先执行两个参数的方法
    	public static void agentmain(String agentArgs, Instrumentation inst) {}
    	public static void agentmain( String agentArgs) {}
    
  • 构建premain和agentmain,创建maven工程,加入如下plugin
 	<plugin>
                <!--mvn assembly:assembly  打包-->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifestEntries>
                            <!--
                            Premain-Class :包含 premain 方法的类(类的全路径名)
                            Agent-Class :包含 agentmain 方法的类(类的全路径名)
                            Boot-Class-Path :设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径。(可选)
                            Can-Redefine-Classes :true表示能重定义此代理所需的类,默认值为 false(可选)
                            Can-Retransform-Classes :true 表示能重转换此代理所需的类,默认值为 false (可选)
                            Can-Set-Native-Method-Prefix: true表示能设置此代理所需的本机方法前缀,默认值为 false(可选)

                            //以下是打包会生成的信息 Premain-Class和Agent-Class必须指定
                            Manifest-Version: 1.0
                            Archiver-Version: Plexus Archiver
                            Created-By: Apache Maven
                            Built-By: Liu Mingyao
                            Build-Jdk: 11.0.3
                            Agent-Class: com.github.mrlawrenc.agent.PreMain
                            Can-Redefine-Classes: true
                            Can-Retransform-Classes: true
                            Premain-Class: com.github.mrlawrenc.agent.PreMain
                            -->
                            <Premain-Class>com.github.mrlawrenc.agent.PreMain</Premain-Class>
                            <Agent-Class>com.github.mrlawrenc.agent.AgentMain</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>

                <executions>
                    <execution>
                        <goals>
                            <goal>attached</goal>
                        </goals>
                        <phase>package</phase>
                    </execution>
                </executions>
            </plugin>
  • 创建com.github.mrlawrenc.agent.PreMain类和com.github.mrlawrenc.agent.AgentMain类,并分别添加premian方法和agentmain方法
public class PreMain {

    /**
     * 该方法在main方法之前运行,与main方法运行在同一个JVM中
     * 并被同一个System ClassLoader装载
     * 被统一的安全策略(security policy)和上下文(context)管理
     * <p>
     * 该方法首先执行,若没有则执行{@link PreMain#premain(String)}
     */
    public static void premain(String agentOps, Instrumentation inst) {
        System.out.println("premain doing..........");
        System.out.println("agent args : " + agentOps);
    }

    /**
     * 如果不存在 premain(String agentOps, Instrumentation inst)
     * 则会执行 premain(String agentOps)
     */
    public static void premain(String agentOps) {
    }
}
public class AgentMain {

    /**
     * <p>
     * 其中整体的执行依赖于VMThread,VMThread是一个在虚拟机创建时生成的单例原生线程,这个线程能派生出其他线程。同时,这个线程的主要的作用是维护一个vm操作队列(VMOperationQueue),用于处理其他线程提交的vm operation,比如执行GC等。
     * <p>
     * VmThread在执行一个vm操作时,先判断这个操作是否需要在safepoint下执行。若需要safepoint下执行且当前系统
     * 不在safepoint下,则调用SafepointSynchronize的方法驱使所有线程进入safepoint中,再执行vm操作。
     * 执行完后再唤醒所有线程。若此操作不需要在safepoint下,或者当前系统已经在safepoint下,则可以直接执行该操作了。
     * 所以,在safepoint的vm操作下,只有vm线程可以执行具体的逻辑,其他线程都要进入safepoint下并被挂起,直到完成此次操作。
     */
    public static void agentmain(String agentArgs, Instrumentation inst) {
		System.out.println("agentmain");
    }
}
  • 打为jar包,使用java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar类型的jar包,可以解压看看里面的META-INF里面的MANIFEST.MF文件,该文件就是描述agent相关信息的,大致如下
	Manifest-Version: 1.0
	Archiver-Version: Plexus Archiver
	Created-By: Apache Maven
	Built-By: Liu Mingyao
	Build-Jdk: 11.0.3
	Agent-Class: com.github.mrlawrenc.agent.AgentMain
	Can-Redefine-Classes: true
	Can-Retransform-Classes: true
	Premain-Class: com.github.mrlawrenc.agent.PreMain
	Agent-Class: com.github.mrlawrenc.agent.AgentMain	

简单使用

  • 启动前通过-javaagent参数使用
    添加启动jvm参数,=后面的为agent命令带入的参数
-javaagent:F:\openSources\dwow\target\java-agent-dwow-1.0-RELEASE-jar-with-dependencies.jar=F:\\openSources\\test\\out\\production\\test

image.png
之后会发现premain方法先于main方法执行

  • 启动之后通过attach方式使用
 public static void main(String[] args) {
        try {
             String agentJarPath = "F:\\openSources\\dwow\\target\\java-agent-dwow-1.0-RELEASE-jar-with-dependencies.jar";

            //main方法名
              String applicationName = "MigrationCoreApplication";
            //查到需要监控的进程
            Optional<String> jvmProcessOpt = Optional.ofNullable(VirtualMachine.list()
                    .stream()
                    .filter(jvm -> {
                        System.out.println("jvm:" + jvm.displayName());
                        return jvm.displayName().contains(applicationName);
                    }).findFirst().get().id());

            if (jvmProcessOpt.isEmpty()) {
                System.out.println("Target Application not found");
                return;
            }

            String jvmPid = jvmProcessOpt.get();
            System.out.println("Attaching to target JVM with PID: " + jvmPid);
            VirtualMachine jvm = VirtualMachine.attach(jvmPid);
            jvm.loadAgent(agentJarPath,agentArgs);
            jvm.detach();
            System.out.println("Attached to target JVM and loaded Java agent successfully");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

结合Instrumentation

  • 简述

    利用 Java 代码,即 java.lang.instrument 做动态 Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能从本地代码中解放出来,使之可以用 Java 代码的方式解决问题。使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。

    在 Java SE 6 里面,instrumentation 包被赋予了更强大的功能:启动后的 instrument、本地代码(native code)instrument,以及动态改变 classpath 等等。

  • 使用

    方法声明如下

    package java.lang.instrument;
    import  java.io.File;
    import  java.io.IOException;
    import  java.util.jar.JarFile;
    
    public interface Instrumentation {
        void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
    
        void addTransformer(ClassFileTransformer transformer);
    
        boolean removeTransformer(ClassFileTransformer transformer);
    
        boolean isRetransformClassesSupported();
    
        void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
    
        boolean isRedefineClassesSupported();
    
        void redefineClasses(ClassDefinition... definitions)
            throws  ClassNotFoundException, UnmodifiableClassException;
    
        boolean isModifiableClass(Class<?> theClass);
    
        @SuppressWarnings("rawtypes")
        Class[] getAllLoadedClasses();
    
        @SuppressWarnings("rawtypes")
        Class[] getInitiatedClasses(ClassLoader loader);
    
        long getObjectSize(Object objectToSize);
    
        void appendToBootstrapClassLoaderSearch(JarFile jarfile);
    
        void appendToSystemClassLoaderSearch(JarFile jarfile);
    
        boolean isNativeMethodPrefixSupported();
    
        void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
    }
    
    
  • redefineClasses和retransformClasses方法

    retransformClasses:会重新执行类的定义和装载,通常配合ClassFileTransformer来使用

    redefineClasses:简单理解为重新装载类,这个类为原始的字节码文件,不会经过ClassFileTransformer#transform方法