Java Agent 的简单使用

在上一篇文章《每天学习一点点之 Spring 计时器 StopWatch》中简单提到了 Java Agent 可以做应用代码的无侵入,也跟朋友进行了讨论。在之前《ThreadLocal 系列之 TransmittableThreadLocal》中,有一个地方没有讨论到的就是 TransmittableThreadLocal 也提供了 Java Agent 的方式。这里简单介绍一下。

Java Agent 也是一个比较早的技术,属于 JVMTI 的一种实现,它的流程跟 AOP 很像,不过它拦截的是 class,这也可以理解为 JVM 事先已经埋了钩子,然后我们根据相关的规范去实现就能触发。这样我们就可以使用 ASM 或者 Javassist 去操作 class,从而实现无侵入性。

Java Agent 有两种启动场景,JVM 启动时和运行时候,接下来介绍 JVM 启动时这种方式,可以配置启动命令进行加载:

-javaagent:<jarpath>[=<options>]

jarpath 就是 JAR 的路径,options 是相关的参数。

The manifest of the agent JAR file must contain the attribute Premain-Class in its main manifest. The value of this attribute is the name of the agent class. The agent class must implement a public static premain method similar in principle to the main application entry point. After the Java Virtual Machine (JVM) has initialized, the premain method will be called, then the real application main method. The premain method must return in order for the startup to proceed.

JAR 的 MANIFEST.MF 文件必须指定 Premain-Class 属性,即指定一个 Premain 类,然后这个类又必须要指定 premain 方法。

The premain method has one of two possible signatures. The JVM first attempts to invoke the following method on the agent class:

public static void premain(String agentArgs, Instrumentation inst)

If the agent class does not implement this method then the JVM will attempt to invoke:

public static void premain(String agentArgs)

premain 方法有两种实现,JVM 会先尝试调用第一个,如果第一个没有实现那么就会调用第二个。Instrumentation 接口是 JDK1.5 之后提供的,我们可以基于它来编写 Agent。

在 main 方法之前执行

这里基于 Maven 进行构建,先创建一个 Agent 类试一试:

public class AgentDemo {

    public static void premain(String arg, Instrumentation instrumentation){
        System.out.println("执行了 premain 方法,arg参数:"+arg);
    }
}

pom.xml 配置为:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.dongguabai</groupId>
    <artifactId>agent-dmo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <build>
        <finalName>agent-demo</finalName>
    </build>
</project>

打包后查看 MANIFEST.MF 文件:

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: dongguabai
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_202

明显与规定不符合,根据官方介绍 agent JAR 可以设置如下属性:

The following manifest attributes are defined for an agent JAR file:

  • Premain-Class

    When an agent is specified at JVM launch time this attribute specifies the agent class. That is, the class containing the premain method. When an agent is specified at JVM launch time this attribute is required. If the attribute is not present the JVM will abort. Note: this is a class name, not a file name or path.

  • Agent-Class

    If an implementation supports a mechanism to start agents sometime after the VM has started then this attribute specifies the agent class. That is, the class containing the agentmain method. This attribute is required if it is not present the agent will not be started. Note: this is a class name, not a file name or path.

  • Launcher-Agent-Class

    If an implementation supports a mechanism to start an application as an executable JAR then the main manifest may include this attribute to specify the class name of an agent to start before the application main method is invoked.

  • Boot-Class-Path

    A list of paths to be searched by the bootstrap class loader. Paths represent directories or libraries (commonly referred to as JAR or zip libraries on many platforms). These paths are searched by the bootstrap class loader after the platform specific mechanisms of locating a class have failed. Paths are searched in the order listed. Paths in the list are separated by one or more spaces. A path takes the syntax of the path component of a hierarchical URI. The path is absolute if it begins with a slash character (’/’), otherwise it is relative. A relative path is resolved against the absolute path of the agent JAR file. Malformed and non-existent paths are ignored. When an agent is started sometime after the VM has started then paths that do not represent a JAR file are ignored. This attribute is optional.

  • Can-Redefine-Classes

    Boolean (true or false, case irrelevant). Is the ability to redefine classes needed by this agent. Values other than true are considered false. This attribute is optional, the default is false.

  • Can-Retransform-Classes

    Boolean (true or false, case irrelevant). Is the ability to retransform classes needed by this agent. Values other than true are considered false. This attribute is optional, the default is false.

  • Can-Set-Native-Method-Prefix

    Boolean (true or false, case irrelevant). Is the ability to set native method prefix needed by this agent. Values other than true are considered false. This attribute is optional, the default is false.

An agent JAR file may have both the Premain-Class and Agent-Class attributes present in the manifest. When the agent is started on the command-line using the -javaagent option then the Premain-Class attribute specifies the name of the agent class and the Agent-Class attribute is ignored. Similarly, if the agent is started sometime after the VM has started, then the Agent-Class attribute specifies the name of the agent class (the value of Premain-Class attribute is ignored).

这时候需要使用 maven-jar-plugin 插件,修改 pom.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.dongguabai</groupId>
    <artifactId>agent-dmo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <build>
        <finalName>agent-demo</finalName>

        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.0.2</version>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <Premain-Class>com.dongguabai.AgentDemo</Premain-Class>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

接下来我创建了一个最简单的 Spring Boot 工程。然后配置启动参数,这个可以在 IDEA 上直接进行配置:

在这里插入图片描述

或者使用命令行的方式(要注意 -javaagent 参数要在 -jar 之前):

java -javaagent:/Users/dongguabai/Desktop/temp/agent-demo/target/agent-demo.jar=hello  -jar /Users/dongguabai/Desktop/temp/demo/target/demo-0.0.1-SNAPSHOT.jar

启动后控制台打印:

➜  demo java -javaagent:/Users/dongguabai/Desktop/temp/agent-demo/target/agent-demo.jar=hello  -jar /Users/dongguabai/Desktop/temp/demo/target/demo-0.0.1-SNAPSHOT.jar 
执行了 premain 方法,arg参数:hello

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.6.RELEASE)

2020-04-29 16:31:31.267  INFO 17572 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication v0.0.1-SNAPSHOT on Dongguabai.local with PID 17572 (/Users/dongguabai/Desktop/temp/demo/target/demo-0.0.1-SNAPSHOT.jar started by dongguabai in /Users/dongguabai/Desktop/temp/demo)

可以看到定义的 AgentDemo 类的 premain 方法已经执行。

计算函数执行时间

这里基于 Java Agent 去实现上一篇文章《每天学习一点点之 Spring 计时器 StopWatch》中的计时功能。这里统计 Spring Boot 项目中 main 方法的执行时长,首先将项目改为非 Web 项目:

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        //SpringApplication.run(DemoApplication.class, args);
        long start = System.currentTimeMillis();
        new SpringApplicationBuilder(DemoApplication.class).web(WebApplicationType.NONE).run(args);
        long end = System.currentTimeMillis();
        System.out.println("---------耗时:" + (end - start));

    }

}

可以看到这里的方式是显示的,如果要做到无侵入,那我们需要去修改 DemoApplication 类,这时候就要用到 Instrumentation 类:

/**
* 添加 class 文件转换器,类装载的时候会调用,如果调用过程出现异常,JVM 会调用其他的转换器,已注册的ClassFileTransformer 将拦截所有应用程序类的加载,并能够访问他们的字节码。ClassFileTransformer 也可以转换应用程序类的字节码,并使JVM的加载行为与原来预期的字节完全不同。
* canRetransform 表示是否允许重新转换,缺省 false;
*
*/
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void addTransformer(ClassFileTransformer transformer);

ClassFileTransformer 是个接口,需要重写这个方法:

/**
* loader:当前的 ClassLoader;
*  className:当前类的名称,注意这里是以 / 分割
*  classBeingRedefined:已经被装载的 Class;
*  protectionDomain:类的域
*   classfileBuffer:类文件原本的字节码
*/
byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;

这个方法返回的就是要装载的 class,如果返回 null 就相当于是没有执行转换,会返回之前的 class。修改 class 最方便的就是使用 Javaassist,在 Agent 项目中引入相关依赖:

<dependency>
			<groupId>org.javassist</groupId>
			<artifactId>javassist</artifactId>
			<version>3.20.0-GA</version>
		</dependency>

接下来目标就是将这么几行代码动态的插入进去:

在这里插入图片描述

Javassist 就是增加代码块,这里就相当于是代理增强了原来的 main 方法:

public class AgentDemo {


    private static final String METHOD_NAME = "main";

    public static void premain(String arg, Instrumentation instrumentation) {
        System.out.println("执行了 premain 方法,arg参数:" + arg);
        instrumentation.addTransformer(new ClassFileTransformer() {
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                                    ProtectionDomain protectionDomain, byte[] classfileBuffer) {

                if ("com/example/demo/DemoApplication".equals(className)) {
                    byte[] newClassFile;
                    ClassPool pool = ClassPool.getDefault();
                    CtClass ct = null;
                    try {
                        pool.insertClassPath(new LoaderClassPath(loader));
                        ct = pool.get(className.replace("/","."));
                        //获取方法
                        CtMethod method = ct.getDeclaredMethod(METHOD_NAME);

                        String oldMethodName = METHOD_NAME + "$old";
                        //旧方法重命名
                        method.setName(oldMethodName);

                        // 复制原来的方法,名字为原来的名字,用来增强之前的方法
                        CtMethod newMethod = CtNewMethod.copy(method, "main", ct, null);

                        // 构建代码块
                        StringBuilder bodyStr = new StringBuilder();
                        bodyStr.append("{");

                        //添加局部变量
                       // newMethod.addLocalVariable("startTime", CtClass.longType);
                        bodyStr.append("long startTime = System.currentTimeMillis();");
                        bodyStr.append("System.out.println(startTime);");

                        // 调用原方法,类似于method();($$)表示所有的参数
                        bodyStr.append(oldMethodName+"($$);\n");

                       // newMethod.addLocalVariable("endTime", CtClass.longType);
                        bodyStr.append("long endTime = System.currentTimeMillis();");
                        bodyStr.append("System.out.println(endTime-startTime);");
                        bodyStr.append("}");

                        //设置新方法方法体
                        newMethod.setBody(bodyStr.toString());

                        // 替换旧方法,增加新方法
                        ct.addMethod(newMethod);

                        newClassFile = ct.toBytecode();
                        System.out.println("22222");
                    } catch (Exception ignored) {
                        ignored.printStackTrace();
                        newClassFile = classfileBuffer;
                    } finally {
                        if (ct != null) {
                            ct.detach();
                        }
                    }
                    return newClassFile;
                }
                return null;
            }
        });
    }
}

出现异常:

Caused by: java.lang.ClassNotFoundException: javassist.ClassPath
        at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        ... 7 more

在网上找了一下,需要修改一下 Agent 的 pom.xml 文件,配置 Boot-Class-Path

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.dongguabai</groupId>
    <artifactId>agent-dmo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>


    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.20.0-GA</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>agent-demo</finalName>

        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.0.2</version>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <Premain-Class>com.dongguabai.AgentDemo</Premain-Class>
                            <Boot-Class-Path>
                                /Users/dongguabai/develope/maven/repository/org/javassist/javassist/3.20.0-GA/javassist-3.20.0-GA.jar
                            </Boot-Class-Path>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Spring Boot 项目的启动类:

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        //SpringApplication.run(DemoApplication.class, args);
        new SpringApplicationBuilder(DemoApplication.class).web(WebApplicationType.NONE).run(args);
    }

}

将两个项目重新打包:

mvn clean package

运行:

➜  demo java -javaagent:/Users/dongguabai/Desktop/temp/agent-demo/target/agent-demo.jar=hello  -jar /Users/dongguabai/Desktop/temp/demo/target/demo-0.0.1-SNAPSHOT.jar 

控制台打印:

➜  demo java -javaagent:/Users/dongguabai/Desktop/temp/agent-demo/target/agent-demo.jar=hello  -jar /Users/dongguabai/Desktop/temp/demo/target/demo-0.0.1-SNAPSHOT.jar 
执行了 premain 方法,arg参数:hello
22222
1588162691595

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.6.RELEASE)

2020-04-29 20:18:12.261  INFO 26265 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication v0.0.1-SNAPSHOT on Dongguabai.local with PID 26265 (/Users/dongguabai/Desktop/temp/demo/target/demo-0.0.1-SNAPSHOT.jar started by dongguabai in /Users/dongguabai/Desktop/temp/demo)
2020-04-29 20:18:12.264  INFO 26265 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
2020-04-29 20:18:12.829  INFO 26265 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'genericWebMvcConfig' of type [com.example.demo.GenericWebMvcConfig$$EnhancerBySpringCGLIB$$4e144799] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-04-29 20:18:12.860  INFO 26265 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'validator' of type [org.hibernate.validator.internal.engine.ValidatorImpl] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-04-29 20:18:12.998  INFO 26265 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 1.133 seconds (JVM running for 1.641)
1404

可以看到,的确是无侵入的增加了时间计算。

References

  • https://docs.oracle.com/javase/10/docs/api/java/lang/instrument/package-summary.html#package.description
  • https://blog.csdn.net/a158123/article/details/76957903
  • https://blogs.oracle.com/ouchina/javaagent
  • https://blog.csdn.net/chao_1990/article/details/70256503

欢迎关注公众号
​​​​​​在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章