skywalking之javaagent
首先说一下javaagent是什么。javaagent是一种能够在不影响正常编译的情况下,修改字节码的技术。JavaAgent 是JDK 1.5 以后引入的,也可以叫做Java代理。JavaAgent 是运行在 main方法之前的拦截器,它内定的方法名叫 premain ,也就是说先执行 premain 方法然后再执行 main 方法。
javaagent是java命令的一个参数。参数 javaagent 可以用于指定一个 jar 包,并且对该 java 包有2个要求:
- 这个 jar 包的MANIFEST.MF 文件必须指定 Premain-Class 项。
- Premain-Class 指定的那个类必须实现 premain()方法。
JavaAgent 实际应用
- 可以在加载java文件之前做拦截把字节码做修改
- 获取所有已经被加载过的类
- 获取某个对象的大小
- 将某个jar加入到bootstrapclasspath里作为高优先级被bootstrapClassloader加载
- 将某个jar加入到classpath里供AppClassload去加载
- 设置某些native方法的前缀,主要在查找native方法的时候做规则匹配
agent案例一
agentdemo1项目
普通maven工程。该项目主要编写premain方法。
package com.lagou.agent;
import java.lang.instrument.Instrumentation;
public class PremainTest1 {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("hello agent demo1");
System.out.println("agentArgs =====> " + agentArgs);
}
}
MANIFREST.MF手工编写
不推荐使用这种方式。比较麻烦,简单了解一下即可。 在 resources 目录下新建目录:META-INF,在该目录下新建文件MANIFREST.MF 。文件内容如下:
Manifest-Version: 1.0
Premain-Class: com.lagou.agent.PremainTest1
Agent-Class: com.lagou.agent.PremainTest1
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Build-Jdk-Spec: 1.8
Created-By: Maven Jar Plugin 3.2.0
这里如果你不去手动指定的话,直接打包,默认会在打包的文件中生成一个MANIFREST.MF文件 。 默认的文件中包含当前的一些版本信息,当前工程的启动类。我们需要增加参数做更多的事情。
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(可选)
MANIFREST.MF 插件一
介绍几种主流的可执行jar包打包插件。
使用maven-jar-plugin插件进行打包。
<build>
<finalName>agentdemo1</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<!--permain方法所在类的完全限定名-->
<Premain- Class>com.lagou.agent.PremainTest1</Premain-Class>
<Agent- Class>com.lagou.agent.PremainTest1</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine- Classes>
<Can-Retransform-Classes>true</Can-Retransform- Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
MANIFREST.MF 插件二
使用maven-assembly-plugin插件进行打包。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with- dependencies</descriptorRef>
</descriptorRefs>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain- Class>com.lagou.agent.PremainTest1</Premain-Class>
<Agent- Class>com.lagou.agent.PremainTest1</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine- Classes>
<Can-Retransform-Classes>true</Can-Retransform- Classes>
</manifestEntries>
</archive>
</configuration>
<!-- 其中<phase>package</phase>、<goal>single</goal> 即表示在执行package打包时,执行assembly:single -->
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
注意事项
选择插件二方式打包时。如果java.exe进程被占用,使用mvn clean package命令或报错。可以直接 在idea插件中assembly中执行assembly:single。或者执行mvn package assembly:single命令。
MANIFREST.MF 插件三
使用maven-shade-plugin插件进行打包。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTr ansformer">
<manifestEntries>
<Premain- Class>com.lagou.agent.PremainTest1</Premain-Class>
</manifestEntries>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
打包命令
mvn clean package
编译工程
选择插件一方式。将agentdemo1项目打包。
mvn clean package
agenttest1项目
普通maven工程。该项目主要编写main方法。
编写main方法
package com.lagou.test1;
public class AgentTest1 {
public static void main(String[] args) {
System.out.println("这里是agent第一个main方法测试。");
}
}
测试main方法
测试main方法是否正常运行。
idea配置
必须先运行一次main,然后点击编辑AgentTest1启动类->edit configurations -> VM options
测试agent一
不带参数测试
-javaagent:D:\ideaworkspaces\202001test\agentdemo1\target\agentdemo1.jar
运行main方法测试:
测试agent二
带参数测试
-javaagent:D:\ideaworkspaces\202001test\agentdemo1\target\agentdemo1.jar=lagou
运行main方法测试:
permain方法
重点就在 premain 方法,也就是我们今天的标题。从字面上理解,就是运行在 main 函数之前的的类。当Java 虚拟机启动时,在执行 main 函数之前,JVM 会先运行 -javaagent 所指定 jar 包内 Premain-Class 这个类的 premain 方法,其中,该方法可以签名如下:
1.public static void premain(String agentArgs, Instrumentation inst)
2.public static void premain(String agentArgs)
JVM 会优先加载 1 签名的方法,加载成功忽略 2,如果1 没有,加载 2 方法。
这个逻辑在sun.instrument.InstrumentationImpl 类中:
InstrumentationImpl 类源码loadClassAndStartAgent方法的217行左右。
private void loadClassAndStartAgent(String var1, String var2, String var3) throws Throwable {
ClassLoader var4 = ClassLoader.getSystemClassLoader();
Class var5 = var4.loadClass(var1);
Method var6 = null;
NoSuchMethodException var7 = null;
boolean var8 = false;
try {
var6 = var5.getDeclaredMethod(var2, String.class, Instrumentation.class);
var8 = true;
} catch (NoSuchMethodException var13) {
var7 = var13;
}
if (var6 == null) {
try {
var6 = var5.getDeclaredMethod(var2, String.class);
} catch (NoSuchMethodException var12) {
}
}
}
package com.lagou.agent;
import java.lang.instrument.Instrumentation;
public class PremainTest1 {
/* public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("hello agent demo1");
System.out.println("agentArgs =====> " + agentArgs);
}*/
public static void premain(String agentArgs) {
System.out.println("hello agent demo2");
System.out.println("agentArgs =====> " + agentArgs);
}
}
agent案例二
agentdemo1项目
package com.lagou.agent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class PremainTest1 {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("hello agent demo1");
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("premain load Class :" + className); return classfileBuffer;
}
}, true);
}
public static void premain(String agentArgs) {
System.out.println("hello agent demo2");
System.out.println("agentArgs =====> " + agentArgs);
}
}
JVMTI(Java Virtual Machine Tool Interface)是一套本地编程接口集合,它提供了一套”代理”程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能。关于 JVMTI 的详细信息,请参考 Java SE 6 文档当中的介绍。
java.lang.instrument 包的实现,也就是基于这种机制的:在 Instrumentation 的实现当中,存在一个JVMTI 的代理程序,通过调用 JVMTI 当中 Java 类相关的函数来完成 Java 类的动态操作。
Instrumentation 的最大作用,就是类定义动态改变和操作。在 Java SE 5 及其后续版本当中,开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 – javaagent 参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。
和类加载器比较
类加载器也可以实现运行时修改代码。但是对代码的侵入性很高。使用 java agent 能让修改字节码这个动作化于无形,对业务透明,减少侵入性。
agent的缺点
需要设置参数javaagent