第03讲:掌握 Java Agent 真的可以为所欲为
Posted Marion158
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第03讲:掌握 Java Agent 真的可以为所欲为相关的知识,希望对你有一定的参考价值。
将 demo-provider 和 demo-webapp 接入 SkyWalking Agent 的时候,只需要在 VM options 中添加下面这一行配置即可:
-javaagent:/path/to/skywalking-agent.jar \\
-Dskywalking_config=/path/to/agent.config
并没有修改任何一行 Java 代码。这里便使用到了 Java Agent 技术,本课时我们将对 Java Agent 技术进行简单介绍并通过示例演示 Java Agent 技术的基本使用。
什么是 Java Agent
Java Agent 是从 JDK1.5 开始引入的,算是一个比较老的技术了。作为 Java 的开发工程师,我们常用的命令之一就是 java 命令,而 Java Agent 本身就是 java 命令的一个参数(即 -javaagent)。正如上一课时接入 SkyWalking Agent 那样,-javaagent 参数之后需要指定一个 jar 包,这个 jar 包需要同时满足下面两个条件:
- 在 META-INF 目录下的 MANIFEST.MF 文件中必须指定 premain-class 配置项。
- premain-class 配置项指定的类必须提供了 premain() 方法。
在 Java 虚拟机启动时,执行 main() 函数之前,虚拟机会先找到 -javaagent 命令指定 jar 包,然后执行 premain-class 中的 premain() 方法。用一句概括其功能的话就是:main() 函数之前的一个拦截器。
使用 Java Agent 的步骤大致如下:
1. 定义一个 MANIFEST.MF 文件,在其中添加 premain-class 配置项。
2. 创建 premain-class 配置项指定的类,并在其中实现 premain() 方法,方法签名如下:
public static void premain(String agentArgs, Instrumentation inst)
...
3. 将 MANIFEST.MF 文件和 premain-class 指定的类一起打包成一个 jar 包。
4. 使用 -javaagent 指定该 jar 包的路径即可执行其中的 premain() 方法。
Java Agent 示例
首先,我们创建一个最基本的 Maven 项目,然后创建 TestAgent.java 这一个类,项目的整体结构如图所示。
TestAgent 中提供了 premain() 方法的实现,如下所示:
public class TestAgent
public static void premain(String agentArgs,
Instrumentation inst)
System.out.println("this is a java agent with two args");
System.out.println("参数:" + agentArgs + "\\n");
public static void premain(String agentArgs)
System.out.println("this is a java agent only one args");
System.out.println("参数:" + agentArgs + "\\n");
premain() 方法有两个重载,如下所示,如果两个重载同时存在,【1】将会被忽略,只执行【2】:
public static void premain(String agentArgs) [1]
public static void premain(String agentArgs,
Instrumentation inst); [2]
代码中有两个参数需要我们注意。
- agentArgs 参数:-javaagent 命令携带的参数。在前面介绍 SkyWalking Agent 接入时提到,agent.service_name 这个配置项的默认值有三种覆盖方式,其中,使用探针配置进行覆盖,探针配置的值就是通过该参数传入的。
- inst 参数:java.lang.instrumen.Instrumentation 是 Instrumention 包中定义的一个接口,它提供了操作类定义的相关方法。
确定 premain() 方法的两个重载优先级的逻辑在 sun.instrument.InstrumentationImpl.java 中实现,相关代码如下:
private void loadClassAndCallPremain(String String optionsString)
throws Throwable
loadClassAndStartAgent( classname, "premain", optionsString );
private void loadClassAndStartAgent(String classname,
String methodname, String optionsString) throws Throwable
... ... // 省略变量定义,下面省略 try/catch代码块
// 查找两个参数的 premain()方法
m = javaAgentClass.getDeclaredMethod( methodname,
new Class<?>[] String.class,
java.lang.instrument.Instrumentation.class);
twoArgAgent = true;
if (m == null) // 查找一个参数的 premain()方法
m = javaAgentClass.getDeclaredMethod(methodname,
new Class<?>[] String.class );
... ...// 省略其他查找
setAccessible(m, true);
// 调用查找到的 premain()重载
if (twoArgAgent)
m.invoke(null, new Object[] optionsString, this );
else
m.invoke(null, new Object[] optionsString );
setAccessible(m, false);
接下来还需要创建 MANIFEST.MF 文件并打包,这里我们直接使用 maven-assembly-plugin 打包插件来完成这两项功能。在 pom.xml 中引入 maven-assembly-plugin 插件并添加相应的配置,如下所示:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4</version>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<!-- 将TestAgent的所有依赖包都打到jar包中-->
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<!-- 添加MANIFEST.MF中的各项配置-->
<manifest>
<!-- 添加 mplementation-*和Specification-*配置项-->
<addDefaultImplementationEntries>true
</addDefaultImplementationEntries>
<addDefaultSpecificationEntries>true
</addDefaultSpecificationEntries>
</manifest>
<!-- 将 premain-class 配置项设置为com.xxx.TestAgent-->
<manifestEntries>
<Premain-Class>com.xxx.TestAgent</Premain-Class>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<!-- 绑定到package生命周期阶段上 -->
<phase>package</phase>
<goals>
<!-- 绑定到package生命周期阶段上 -->
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
最后执行 maven 命令进行打包,如下:
mvn package -Dcheckstyle.skip -DskipTests
完成打包之后,我们可以解压 target 目录下的 test-agent.jar,在其 META-INF 目录下可以找到 MANIFEST.MF 文件,其内容如下:
Manifest-Version: 1.0
Implementation-Title: TestAgent
Premain-Class: com.xxx.TestAgent # 关注这一项
Implementation-Versioclearn: 1.0.0-SNAPSHOT
Archiver-Version: Plexus Archiver
Built-By: xxx
Specification-Title: TestAgent
Implementation-Vendor-Id: com.xxx
Created-By: Apache Maven 3.6.0
Build-Jdk: 1.8.0_191
Specification-Version: 1.0.0-SNAPSHOT
到此为止,Java Agent 使用的 jar 包创建完成了。
下面再创建一个普通的 Maven 项目:TestMain,项目结构与 TestAgent 类似,如图所示:
在 Main 这个类中定义了该项目的入口 main() 方法,如下所示:
public class Main
public static void main(String[] args) throws Exception
Thread.sleep(1000);
System.out.println("TestMain Main!");
在启动 TestMain 项目之前,需要在 VM options 中使用 -javaagent 命令指定前面创建的 test-agent.jar,如图所示:
启动 TestMain 之后得到了如下输出:
this is a java agent.
参数:option1=value2,option2=value2
TestMain Main!
修改类实现
Java Agent 可以实现的功能远不止添加一行日志这么简单,这里需要关注一下 premain() 方法中的第二个参数:Instrumentation。Instrumentation 位于 java.lang.instrument 包中,通过这个工具包,我们可以编写一个强大的 Java Agent 程序,用来动态替换或是修改某些类的定义。
下面先来简单介绍一下 Instrumentation 中的核心 API 方法:
- addTransformer()/removeTransformer() 方法:注册/注销一个 ClassFileTransformer 类的实例,该 Transformer 会在类加载的时候被调用,可用于修改类定义。
- redefineClasses() 方法:该方法针对的是已经加载的类,它会对传入的类进行重新定义。
- **getAllLoadedClasses()方法:**返回当前 JVM 已加载的所有类。
- getInitiatedClasses() 方法:返回当前 JVM 已经初始化的类。
- getObjectSize()方法:获取参数指定的对象的大小。
下面我们通过一个示例演示 Instrumentation 如何与 Java Agent 配合修改类定义。首先我们提供一个普通的 Java 类:TestClass,其中提供一个 getNumber() 方法:
public class TestClass
public int getNumber() return 1;
编译生成 TestClass.class 文件之后,我们将 getNumber() 方法返回值修改为 2,然后再次编译,并将此次得到的 class 文件重命名为 TestClass.class.2 文件,如图所示,我们得到两个 TestClass.class 文件:
之后将 TestClass.getNumber() 方法返回值改回 1 ,重新编译。
然后编写一个 main() 方法,新建一个 TestClass 对象并输出其 getNumber() 方法的返回值:
public class Main
public static void main(String[] args)
System.out.println(new TestClass().getNumber());
接下来编写 premain() 方法,并注册一个 Transformer 对象:
public class TestAgent
public static void premain(String agentArgs, Instrumentation inst)
throws Exception
// 注册一个 Transformer,该 Transformer在类加载时被调用
inst.addTransformer(new Transformer(), true);
inst.retransformClasses(TestClass.class);
System.out.println("premain done");
Transformer 实现了 ClassFileTransformer,其中的 transform() 方法实现可以修改加载到的类的定义,具体实现如下:
class Transformer implements ClassFileTransformer
public byte[] transform(ClassLoader l, String className,
Class<?> c, ProtectionDomain pd, byte[] b)
if (!c.getSimpleName().equals("TestClass"))
return null; // 只修改TestClass的定义
// 读取 TestClass.class.2这个 class文件,作为 TestClass类的新定义
return getBytesFromFile("TestClass.class.2");
之后还需要在 maven-assembly-plugin 插件中添加 Can-Retransform-Classes 参数:
// 省略其他配置
<manifestEntries>
<Premain-Class>com.xxx.TestAgent</Premain-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
最后,打包启动应用,得到的输出如下:
premain done
2
# 此输出说明,类TestClass的定义在加载时已经被 Transformer替换成
# 文件 TestClass.class.2 中的定义
统计方法耗时
介绍完 Java Agent 的基本使用流程之后,这里做一个简单的进阶示例,将 Java Agent 与 Byte Buddy 结合使用,统计 com.xxx 包下所有方法的耗时。
回到 TestAgent 项目,整个项目的结构不变,需要在 pom.xml 中添加 Byte Buddy 的依赖,如下所示:
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.9.2</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.9.2</version>
</dependency>
Byte Buddy 是一个开源 Java 库,其主要功能是帮助用户屏蔽字节码操作,以及复杂的 Instrumentation API 。Byte Buddy 提供了一套类型安全的 API 和注解,我们可以直接使用这些 API 和注解轻松实现复杂的字节码操作。另外,Byte Buddy 提供了针对 Java Agent 的额外 API,帮助开发人员在 Java Agent 场景轻松增强已有代码。在 下一课时中,我们将深入介绍 SkyWalking 涉及的 Byte Buddy 的 API,这里不做深入研究,只对此处涉及的 API 和注解做简单说明。
接下来对 TestAgent.java 进行修改,使用 Byte Buddy 提供的 API 拦截指定的类和方法,如下所示:
public class TestAgent
public static void premain(String agentArgs,
Instrumentation inst)
// Byte Buddy会根据 Transformer指定的规则进行拦截并增强代码
AgentBuilder.Transformer transformer =
new AgentBuilder.Transformer()
public DynamicType.Builder<?> transform(
DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassLoader classLoader,
JavaModule module)
// method()指定哪些方法需要被拦截,ElementMatchers.any()表 // 示拦截所有方法
return builder.method(
ElementMatchers.<MethodDescription>any())
// intercept()指明拦截上述方法的拦截器
.intercept(MethodDelegation.to(
TimeInterceptor.class));
;
// Byte Buddy专门有个AgentBuilder来处理Java Agent的场景
new AgentBuilder
.Default()
// 根据包名前缀拦截类
.type(ElementMatchers.nameStartsWith("com.xxx"))
// 拦截到的类由transformer处理
.transform(transformer)
.installOn(inst); // 安装到 Instrumentation
当拦截到符合条件的类时,会交给我们的 AgentBuilder.Transformer 实现处理,当 Transformer 拦截到符合条件的方法时,会交给上面指定的 TimeInterceptor 处理。TimeInterceptor 的具体实现如下:
public class TimeInterceptor
@RuntimeType
public static Object intercept(@Origin Method method,
@SuperCall Callable<?> callable) throws Exception
long start = System.currentTimeMillis();
try
return callable.call(); // 执行原函数
finally
System.out.println(method.getName() + ":"
+ (System.currentTimeMillis() - start) + "ms");
TimeInterceptor 就类似于 AOP 的环绕切面。这里通过 @SuperCall 注解注入的 Callable 实例可以调到被拦截的目标方法,需要注意的是,在通过 Callable 调用目标方法时,即使目标方法带参数,这里也不用显式的传递。这里 @Origin 注解注入了被拦截方法对应的 Method 对象。
完成 TestAgent 的修改之后,执行如下命令,重新打包 test-agent.jar:
mvn clean package -Dcheckstyle.skip -DskipTests
打包完成,回到 TestMain 项目,所有代码和配置都无需修改,直接启动,会得到输出如下:
TestMain Main!
main:1003ms
Attach API 基础
在 Java 5 中,Java 开发者只能通过 Java Agent 中的 premain() 方法在 main() 方法执行之前进行一些操作,这种方式在一定程度上限制了灵活性。Java 6 针对这种状况做出了改进,提供了一个 agentmain() 方法,Java 开发者可以在 main() 方法执行以后执行 agentmain() 方法实现一些特殊功能。
agentmain() 方法同样有两个重载,它们的参数与 premain() 方法相同,而且前者优先级也是高于后者的:
public static void agentmain (String agentArgs,
Instrumentation inst);[1]
public static void agentmain (String agentArgs); [2]
agentmain() 方法主要用在 JVM Attach 工具中,Attach API 是 Java 的扩展 API,可以向目标 JVM “附着”(Attach)一个代理工具程序,而这个代理工具程序的入口就是 agentmain() 方法。
Attach API 中有 2 个核心类需要特别说明:
- VirtualMachine 是对一个 Java 虚拟机的抽象,在 Attach 工具程序监控目标虚拟机的时候会用到该类。VirtualMachine 提供了 JVM 枚举、Attach、Detach 等基本操作。
- VirtualMachineDescriptor 是一个描述虚拟机的容器类,后面示例中会介绍它如何与 VirtualMachine 配合使用。
下面的示例依然通过用类文件替换的方式修改 TestClass 这个类的返回值。首先,前文使用到的 TestClass,以及 TestClass.class.2 文件不变。main() 方法略作修改,其中每隔 1s 创建一个新的 TestClass 对象并输出 getNumber() 方法返回值,具体实现如下:
public static void main(String[] args) throws InterruptedException
System.out.println(new TestClass().getNumber());
while (true)
Thread.sleep(1000); // 注意,这里是新建TestClass对象
System.out.println(new TestClass().getNumber());
另外,将 TestAgent 中的 premain() 方法修改成 agentmain() 方法(除了名称变化,没有任何其他变化)。
需要再添加一个 AttachMain 类,其中会通过 Attach API 监听上面 main() 方法的启动,这里每隔 1s 检查一次所有的 Java 虚拟机,当发现有新的虚拟机出现的时候,就调用 attach() 方法,将 TestAgent 所在的 jar 包“附加”上去,“附加”成功之后,TestAgent 就会通过 Transformer 修改TestClass 类定义。AttachMain 的具体实现如下:
public class AttachMain
public static void main(String[] args) throws Exception
List<VirtualMachineDescriptor> listBefore =
VirtualMachine.list();
String jar = ".../test-agent.jar"; // agentmain()方法所在jar包
VirtualMachine vm = null;
List<VirtualMachineDescriptor> listAfter = null;
while (true)
listAfter = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : listAfter)
if (!listBefore.contains(vmd)) // 发现新的JVM
vm = VirtualMachine.attach(vmd); // attach到新JVM
vm.loadAgent(jar); // 加载agentmain所在的jar包
vm.detach(); // detach
return;
Thread.sleep(1000);
在 pom.xml 文件中添加如下依赖,否则在编译的过程中会抛异常:
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>$java.home/../lib/tools.jar</systemPath>
</dependency>
另外,还要在 MANIFEST.MF 文件中添加 Agent-Class 这一项, pom.xml 文件中具体配置如下:
<manifestEntries>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<!-- 指向agentmain()方法所在的类 -->
<Agent-Class>com.xxx.TestAgent</Agent-Class>
</manifestEntries>
最后,我们先启动 AttachMain 类,然后通过命令行启动 Main :
java -cp ./test-agent.jar com.xxx.attach.Main
-------
输出:
1
premain done # attach成功,Transformer会修改TestClass的定义
2 # 修改后的TestClass.getNumber()方法返回值为 2
2
在 SkyWalking Agent 中并没有使用到 Attach API,但是作为 Java Agent 的扩展,还是希望你对其有所了解。
总结
本课时重点介绍了 Java Agent 技术的基础知识,然后通过 TestAgent 和 TestMain 两个示例项目简单展示了 Java Agent 技术的使用流程,之后通过 Java Agent 和 Byte Buddy 实现了统计方法耗时的功能,最后还简单介绍 Attach API 的相关功能并进行了示例演示。
希望你在课后可以自己多加练习。
精通Framework是真的可以为所欲为
近十几年来,随着以Android系统为代表的智能手机普及与发展,互联网行业早已进入“移动”的时代。但是现如今的“风口”已经从移动转向,整个移动互联网行业正处于增量下降、存量厮杀的阶段。面对技术更新迭代加速,前景不太明朗,很多开发者都感到了有些焦虑和迷茫。并且,在如此的大环境下,整个行业头部企业,越来越重视产品的体验与成本,对中高级的开发者的能力要求也越来越高。
现在大厂面试时,我们经常会被问到这些问题:
- 为什么Zygote通信fork进程,使用的是socket,而不是Android的Binder?
- 为什么是从zygote进程fork App,而不是其他进程?
- Binder在做数据传输过程中,最大的数据量限制是多少?
- 打开一个Activity的过程中经历过几次跨进程调用?
- ANR弹框的原理是什么?
- ……
每当这时候,内心真是一万只槽泥马奔腾而过……
大部分Android开发者一遇到这种面试题就直接懵逼了,不少人不是没有看过相关的解答,但也都只是浅尝辄止,没有深入掌握其中原理,面试的时候自然会被问个措手不及。
下面这张图想必大家都看过,Google官方提供过一张经典的平台架构图,从下往上依次分为:Linux内核、硬件抽象层、Native层、Java Framework层、App层,每一层都包含大量的子模块或子系统。
可以看到具体app的下面就是Framework层的支撑。所以掌握Framework层非常有助于我们开发出一个性能良好的App,另外在大厂的面试过程中,Framework也是高阶面试时必问的问题:
在所有的Framework知识中,要数最重要的还是AMS,主打和Activity,Service,ContentProvider,Broadcast等交互:
看一下上图,Activity启动,涉及到ActivityThread,AMS,H类,上述过程还涉及到多次跨进程调用,涉及到各种binder的知识。
搞清楚这些:我们就可以去研究各种黑科技,例如在做插件化的时候,你需要占坑Activity等,hook代码等都是在和AMS斗智斗勇;在做性能优化的时候,你也要了解AMS是如何调度Activity的,消息队列是如何运转的。
**但AMS本身比较复杂、难以理解,许多工作多年的Android开发者也很难弄清AMS的作用。**于是,系统的整体运行过程就成为了大厂面试的重灾区。
比如下面这张Android启动流程图,不少人都看过,但少有人沉下心去仔仔细细的研究过。
作为过来人,我发现很多学习者和实践者都在 Android Framework上面临着很多的困扰,比如:
- 工作场景中遇到难题,往往只能靠盲猜和感觉,用临时性的补救措施去掩盖,看似解决了问题,但下次同样的问题又会发作,原因则是缺乏方法论、思路的指引以及工具支持;
- 能力修炼中,缺乏互联网项目这一实践环境,对Framework只能通过理论知识进行想象,无法认识其在工作实战中的真实面目和实操过程;
- 职场晋升中,只管功能开发,不了解底层原理,缺少深入地思考与总结,无法完成复杂系统设计这类高阶工作,难以在工作中大展拳脚,而有挑战的工作往往留给有准备的人。
总之,一旦遇到问题,很少人能够由点及面逆向分析,最终找到瓶颈点和最优解决方案,而Framework是Android开发的深水区,也是衡量一个Android程序员能力高低的标准。
如果你还没有掌握Framework,现在想要在最短的时间里吃透它,那么必须要跟着正确的学习路线学习!
这里给大家推荐一套学习路线,并附有相关《Framework核心知识点笔记》,相信可以给大家提供一些帮助,有需要的朋友们也可以下领取一下随时查漏补缺。
《Framework核心知识点笔记》已经进行了整理上传至公号中:Android开发之家,可以自行访问查阅。
《Framework核心知识点笔记》已经进行了整理上传至公号中:Android开发之家,可以自行访问查阅。
以上是关于第03讲:掌握 Java Agent 真的可以为所欲为的主要内容,如果未能解决你的问题,请参考以下文章