黑魔法 JavaAgent,还有不会的吗 ?

小哈学Java

共 19596字,需浏览 40分钟

 ·

2022-02-26 16:15

点击关注公众号,Java干货及时送达👇





1
JavaAgent是什么




Java agent 是 JDK1.5 中引入的一个强大工具,它在分析活动中非常有用,开发人员和应用程序用户可以在这些活动中监视在服务器和应用程序中执行的任务。

Agent 基于 JVMTI 接口规范,提供了一种在加载字节码时,对字节码进行修改的方式,他共有两种方式执行,一种是在 main 方法执行之前,通过 premain 来实现,另一种是在程序运行中,通过 attach api 来实现。

这门技术对大多数人来说都比较陌生,就像一个黑盒子,但是日常工作中,又或多或少接触过基于 JavaAgent 技术实现的工具。



如上图,这上面的一些工具都是基于 JavaAgent 来实现的,还有大名鼎鼎的 Skywalking 和 ja-netfilter,它们基本上对你的业务没有入侵,但功能都很强悍,用起来非常爽。

知其然也要知其所以然,用的舒服的同时,我们也要搞清楚它们是怎么工作的,只有把工作原理和流程弄清楚了,在使用的过程中才不会迷糊。

今天就带大家来入个门,了解一下 JavaAgent 到底是怎么玩的,磨刀不误砍柴,我们先将 JavaAgent 中的一些基本概念搞清楚,请大家耐心看完,这样在后面的讲解中才不会一脸懵逼。



2
基础概念




premain



这个 premain 方法跟我们以往使用的一些 API 不太一样,它并没有一些父类和接口预先定义好,然后我们自己去复写实现,而是固定写死的(不能写错),在目标程序启动的时候,它会先于目标程序加载。


package com.wolffy.hello;
import java.lang.instrument.Instrumentation;
/** * 预先处理,程序启动时优先加载,JavaAgent.class - By 「Java者说」 -> https://www.jiweichengzhu.com/ * Created by wolffy on 2022/2/15. */public class HelloPremain {
/** * premain()有两种写法,Instrumentation参数可以不传递,带有Instrumentation参数的方法优先级更高 * * @param agentArgs 字符串参数,可以在启动的时候手动传入 * @param inst 此参数由jvm传入,Instrumentation中包含了对class文件操作的一些api,可以让我们基于此来进行class文件的编辑 */ public static void premain(String agentArgs, Instrumentation inst) { System.out.println("Yes, I am a real Agent for premain Class."); }
/** * 这个方法没有上面那个方法的优先级高,程序运行时会优先找上面那个方法,如果没找到,才会用这个方法 * * @param agentArgs 字符串参数,可以在启动的时候手动传入 */ public static void premain(String agentArgs) { System.out.println("Yes, I am a real Agent Class."); }
}


agentmain



如果说 premain 是预先处理,那么与之相对应的 agentmain 就可以理解为后置处理,无需在程序启动的时候就指定它,而是可以在程序启动之后,再利用 attach api 来加载它。


package com.wolffy.hello;
import java.lang.instrument.Instrumentation;
/** * 后置处理,启动时无需加载,可在程序启动之后进行加载,JavaAgent.class - By 「Java者说」 -> https://www.jiweichengzhu.com/ * Created by wolffy on 2022/2/15. */public class HelloAgentmain {
/** * agentmain()有两种写法,Instrumentation参数可以不传递,带有Instrumentation参数的方法优先级更高 * * @param agentArgs 字符串参数,可以在启动的时候手动传入 * @param inst 此参数由jvm传入,Instrumentation中包含了对class文件操作的一些api,可以让我们基于此来进行class文件的编辑 */ public static void agentmain(String agentArgs, Instrumentation inst) { System.out.println("Yes, I am a real Agent for agentmain Class."); }
/** * 这个方法没有上面那个方法的优先级高,程序运行时会优先找上面那个方法,如果没找到,才会用这个方法 * * @param agentArgs 字符串参数,可以在启动的时候手动传入 */ public static void agentmain(String agentArgs) { System.out.println("Yes, I am a real Agent Class."); }
}


Instrumentation 



大家重点看 premain 和 agentmain 的第二个参数,如果我们想要在后续对 java 字节码进行修改,那么就必须通过 Instrumentation 来实现,它由 JVM 传入,是 JDK1.5 提供的 API,用于拦截类加载事件,并对字节码进行修改。


Instrumentation 的功能非常强悍,提供了大概有十几个 API,若我们只想要修改字节码的话,就关心其中的三个主要方法即可。


public interface Instrumentation {    //注册一个转换器,类加载事件会被注册的转换器所拦截     void addTransformer(ClassFileTransformer transformer, boolean canRetransform);    //重新触发类加载     void retransformClasses(Class... classes) throws UnmodifiableClassException;    //直接替换类的定义     void redefineClasses(ClassDefinition... definitions) throws  ClassNotFoundException, UnmodifiableClassException;}


ClassFileTransformer



Instrumentation 中又引入了一个新的 ClassFileTransformer  类,名字取的非常语义化,一眼就能看出来是用来转换 class 文件的。


package com.wolffy.hello;
import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.ProtectionDomain;
/** * Transformer.class - By 「Java者说」 -> https://www.jiweichengzhu.com/ * Created by wolffy on 2022/2/15. */public class HelloTransformer implements ClassFileTransformer {
/** * 转换提供的类文件并返回一个新的替换类文件 * * @param loader 要转换的类的定义加载器,如果引导加载器可能为null * @param className Java 虚拟机规范中定义的完全限定类和接口名称的内部形式的类名称。例如, "java/util/List" 。 * @param classBeingRedefined 如果这是由重新定义或重新转换触发的,则该类被重新定义或重新转换;如果这是一个类加载, null * @param protectionDomain 被定义或重新定义的类的保护域 * @param classfileBuffer 类文件格式的输入字节缓冲区 - 不得修改 * @return 转换后的字节码数组 * @throws IllegalClassFormatException class文件格式异常 */ @Override public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// 实现ClassFileTransformer接口之后,这里默认是return了一个[]空数组 // 千万注意: // 如果不需要改变,那就return null,如果return了一个[],那么你的class就会被置空,程序就会报错
// return new byte[0];
return null; }
}


理论太枯燥,所以每一个解释下面我都放了实战代码来给大家做演示,这样会更好理解一点儿,这几个基础概念,大家先简单看一下,有一个大概的印象之后,接下来通过具体的代码和案例来给大家讲解。




3
JavaAgent使用方式




在实战之前,我们来了解一下 JavaAgent 的使用方式。


正常我们运行一个 java 程序,都是需要找到一个 main 方法入口,如果是 jar 包的话,一般都是直接 java -jar


如果我们是用的是 premain 方式,那我们直接通过追加 -javaagent 参数来引入 agent。



java -javaagent:/Users/wolffy/agent.jar -jar /Users/wolffy/test.jar


如果我们是用的是 agent 方式,那么就需要借助于 JDK 的 tools.jar 中的 API 了。


// 连接jvm,并利用相关的api找到HelloTest工程运行时的进程id,也就是PIDVirtualMachine vm = VirtualMachine.attach("12345");// 加载agent,大家注意使用自己的路径vm.loadAgent("/Users/wolffy/agent.jar");// 脱离jvmvm.detach();



4
实战 - premain




我们来创建两个 java 工程,一个工程中含有 premain 方法的类,一个工程就用来打包测试 jar 包。

当我们编写了一个 JavaAgent 之后,想让它被加载用起来,必须要有一份 META-INF/MANIFEST.MF 文件作为指引


Manifest-Version: 1.0Premain-Class: com.wolffy.hello.HelloAgentCan-Redefine-Classes: trueCan-Retransform-Classes: trueCan-Set-Native-Method-Prefix: trueBuild-Jdk-Spec: 1.8Created-By: Maven Jar Plugin 3.2.0Main-Class: com.wolffy.hello.HelloMain


这种文件编写起来不是很麻烦,但是有更方便的 Maven 了,所以就不再折腾手写了,本文用来演示的 demo 也是构建的 maven 工程。




新建的 agent 工程名为 HelloAgent,路径为D:\workspace_idea\HelloAgent



工程中包含 premain 方法的类是 HelloPremain.java


package com.wolffy.hello;
import java.lang.instrument.Instrumentation;
/** * 预先处理,程序启动时优先加载,JavaAgent.class - By 「Java者说」 -> https://www.jiweichengzhu.com/ * Created by wolffy on 2022/2/15. */public class HelloPremain {
public static void premain(String agentArgs, Instrumentation inst) { System.out.println("Yes, I am a real Agent for premain Class."); }
}


pom.xml 文件中需要配置 Premain-Class,其中 mainClass 可以省略,因为 agent 是附着在其他应用上的,类似于寄生虫一样,所以它本身能不能单独运行关系不大。


    org.apache.maven.plugins    maven-jar-plugin    3.2.0                                                        com.wolffy.hello.HelloMain                                                        com.wolffy.hello.HelloPremain

true true true




新建的 test 工程名为 HelloTest,路径为D:\workspace_idea\HelloTest



工程中包含 main 方法运行入口的类是 AgentTest.java


package com.wolffy.hello.test;
/** * AgentTest.class - By 「Java者说」 -> https://www.jiweichengzhu.com/ * Created by wolffy on 2022/2/15. */public class AgentTest {
public static void main(String[] args) { System.out.println("hello, here is hello test for agent."); }
}


pom.xml中打包的配置只需要指定 jar 运行的 mainClass 入口


    org.apache.maven.plugins    maven-jar-plugin    3.2.0                                                        com.wolffy.hello.test.AgentTest                        


两个工程的代码编写完毕,我们将其打成2个jar包:HelloAgent.jar、HelloTest.jar

由于我都为其指定了 mainClass,所以我把两个 jar 都执行一下看看输出的内容



如上图,这是运行 agent 工程的 jar 文件输出的内容



如上图,这是运行 test 工程的 jar 文件输出的内容。

现在我给 test 工程引入 agent 来看一下效果,直接在 java -jar 中间加入 -javaagent 参数。



如上图,可以看到有两行字符串输出,第二行输出的文案是在 test 工程的 main 方法入口中定义的,而第一行输出的文案,则不是在 test 工程中定义的,而是在 agent 工程中定义的。

这么看起来,是不是很离谱?

test 中根本没有定义,但是它却输出了,仅仅是在启动的时候配置了 -javaagent,可以看到,对 test 的业务代码根本没有任何侵入。



好了,通过这个简单的字符串打印程序,我们已经可以很清晰的看到 premain 的工作模式了,只要你引入了 agent,那么它一定会在你的应用程序启动之前执行。



5
实战 - agentmain



虽然 premain 方式使用起来简单便捷,但是有一个致命的问题,因为它在应用程序启动之前执行,一旦它出现了问题,就会导致应用程序也启不来,所以必须保证 agent 程序100%可用

在 Jdk1.6 的时候,agentmain 出场了,这种方式它不要求在应用程序启动之前就加载,可以在程序运行中途进行加载,比如我们常用的 Arthas 和 Skywalking,它们就是在应用程序启动之后,再来收集信息做监控和诊断。

agentmain 要想被使用起来,也是需要有一份 META-INF/MANIFEST.MF 文件的,用来指定 agent class 文件位置。


Manifest-Version: 1.0Agent-Class: com.wolffy.hello.HelloAgentmainCan-Redefine-Classes: trueCan-Retransform-Classes: trueCan-Set-Native-Method-Prefix: trueBuild-Jdk-Spec: 1.8Created-By: Maven Jar Plugin 3.2.0Main-Class: com.wolffy.hello.HelloMain




光说不练,等于白干,我们来手动写一个小案例模拟一下 arthas,这样大家也能看的更加清晰,以小看大,就能明白 arthas 这种工具的工作原理。



如上图,我用红色字体将 arthas 的工作流程简单总结了一下,我们也来简单模拟一下这几步流程。

工程就不再新建了,直接复用之前的,test 工程依旧是 HelloTest,只不过我们为了模拟程序真实运行,所以加了一个死循环让其长时间运行。


package com.wolffy.hello.test;
/** * AgentTest.class - By 「Java者说」 -> https://www.jiweichengzhu.com/ * Created by wolffy on 2022/2/15. */public class AgentTest {
public static void main(String[] args) { System.out.println("hello, here is hello test for agent.");
while (true) { // 模拟应用程序,让其长时间运行 } }
}


agent 工程依旧是 HelloAgent,只不过我们在 HelloAgent 工程中启动了一个 socket 服务来接收数据,并加入 agentmain 的代码


package com.wolffy.hello;
import com.sun.tools.attach.AgentInitializationException;import com.sun.tools.attach.AgentLoadException;import com.sun.tools.attach.AttachNotSupportedException;import com.sun.tools.attach.VirtualMachine;
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.net.ServerSocket;import java.net.Socket;import java.net.SocketAddress;import java.util.Scanner;
/** * Main.class - By 「Java者说」 -> https://www.jiweichengzhu.com/ * Created by wolffy on 2022/2/15. */public class HelloMain {
public static void main(String[] args) { System.out.println("Hello, I am a JavaAgent demo, created by https://www.jiweichengzhu.com/\n");
try { // 创建socket服务器 ServerSocket server = new ServerSocket(9876); System.out.println("启动Socket Server完毕,开始监听9876端口\n");
// 选择java应用程序的pid Scanner scanner = new Scanner(System.in); System.out.print("请输入PID: ");
String pid = scanner.next(); System.out.println();
// 模拟arthas选择pid动作 + 加载agent simulationAndLoad(pid); System.out.println("为PID=" + pid + "的应用程序加载agent完毕\n");
// 接收socket客户端链接,这里会阻塞,直到有客户端连接上来 Socket client = server.accept();
// 获取客户端远程地址信息 SocketAddress address = client.getRemoteSocketAddress(); System.out.println("客户端[" + address + "]已连接\n");
String msg = receiveMsg(client); System.out.println("客户端[" + address + "]说: " + msg + "\n");
} catch (IOException e) { e.printStackTrace(); } }
/** * 模拟arthas选择pid动作 + 加载agent * * @param pid 进程ID */ private static void simulationAndLoad(String pid) { try { // 连接jvm,并利用相关的api找到HelloTest工程运行时的进程id,也就是PID VirtualMachine vm = VirtualMachine.attach(pid); // 加载agent,大家注意使用自己的路径 vm.loadAgent("D:\\workspace_idea\\HelloAgent\\target\\HelloAgent.jar"); // 脱离jvm vm.detach(); } catch (AttachNotSupportedException | IOException | AgentLoadException | AgentInitializationException e) { e.printStackTrace(); } }
/** * 接收客户端消息,只做演示使用,所以只使用一次就行了 * * @param socket 客户端链接 * @return 客户端发来的消息 * @throws IOException IO异常 */ private static String receiveMsg(Socket socket) throws IOException { // 打开客户端的输入流 InputStream is = socket.getInputStream(); // 创建字节流到字符流的桥接 InputStreamReader isr = new InputStreamReader(is); // 借助缓存流来进行缓冲文本读取 BufferedReader br = new BufferedReader(isr); // 读取一个文本行 String msg = br.readLine();
// 关闭IO br.close(); isr.close(); is.close();
return msg; }
}


从上面可以看到,main 入口的代码很简单,就是一个简单的 ServerSocket,好让应用程序能连接过来上报数据。


package com.wolffy.hello;
import java.io.IOException;import java.io.OutputStream;import java.io.PrintWriter;import java.lang.instrument.Instrumentation;import java.net.Socket;
/** * 后置处理,启动时无需加载,可在程序启动之后进行加载,JavaAgent.class - By 「Java者说」 -> https://www.jiweichengzhu.com/ * Created by wolffy on 2022/2/15. */public class HelloAgentmain {
public static void agentmain(String agentArgs, Instrumentation inst) { System.out.println("Yes, I am a real Agent for agentmain Class.");
try { // 连接socket服务端 Socket socket = new Socket("127.0.0.1", 9876);
// 打开输出流 OutputStream os = socket.getOutputStream();
// 格式化输出流,自带刷新 PrintWriter pw = new PrintWriter(os, true);
String project = System.getProperty("user.dir"); pw.println("hay, i am project [" + project + "]");
// 关闭IO pw.close(); os.close(); socket.close();
} catch (IOException e) { e.printStackTrace(); } }
}


agentmain 的代码也很简单,连接上 socket 服务之后,进行数据的上报。

关于 attach api 中传入的 pid,大家可以直接在本机进程中查看,找到 HelloTest 程序运行的那个 java 进程,将其的 pid 复制过来就行。



如上图,我已经将 HelloTest 启动了,大家可以使用 jps 命令查看 java 进程,如果是 windows 系统,也可以直接使用任务管理器。



如上图,可以很清楚的找到 HelloTest 的进程,前面那个数据就是它的 PID,其他几个是我机器上的其他 java 应用程序。



如上图,这是 windows 任务管理器的界面,在里面也能找到 HelloTest 的应用程序的 PID,就是如果有多个 java 程序的话,任务管理器就不太直观了,所以推荐大家使用 jps 命令。

测试的时候,我们先启动 test 工程,然后再启动 agent 工程,只有这样才能拿到 test 工程的 PID。



如上图,是我将 HelloAgent 启动之后看到的日志,根据日志打印的顺序来看, 是不是跟 arthas 很像?



如上图,这是启动了 HelloAgent 工程之后,在 HelloTest 工程中看到的输出内容,是在 test 自身输出之后打印的。

ok,我们简单模拟 arthas 已经完成,虽然只是一只小小的麻雀,但它的五脏俱全,还是很能体现 agentmain 的工作过程的。



呐,现在已经基于 premain 和 agentmain 各自实现了一套代码,也分别测试了效果,从测试结果来看,我们可看到这二者各自适合的使用场景:

1、premain 适合用在激活各类 java 应用中;
2、agentmain 适用在收集各类 java 应用的数据中;

在一般的数据收集场景中,被收集方要主动上报数据给收集方,那么它就必须要在代码中写一段上报的逻辑,但是大家可以看到,HelloTest 工程中并没有写这样的代码,而是直接由 agent 来完成的,对 test 的代码没有任何的侵入,非常的犀利。




6
实战 - ClassFileTransformer



上面将 premain 和 agentmain 讲完了,这都是最基础的功能,我们平时使用的一些功能强大的工具也都是基于这个 JavaAgent,正所谓万丈高楼平地起,基础打好了就不害怕了。

现在我们来看一下上面提到的 ClassFileTransformer,我们用它来修改一个 class 文件看看效果(premian 方式)。

test 工程不变,依旧只是输出一段文字内容,我们待会儿利用 agent 来将这行文字给篡改了。


package com.wolffy.hello.test;
/** * AgentTest.class - By 「Java者说」 -> https://www.jiweichengzhu.com/ * Created by wolffy on 2022/2/15. */public class AgentTest {
public static void main(String[] args) { System.out.println("hello, here is hello test for agent.");
// while (true) { // // 模拟应用程序,让其长时间运行 // } }
}


agent 工程中,我们自定义一个 Transformer 类


package com.wolffy.hello;
import jdk.internal.org.objectweb.asm.ClassWriter;import jdk.internal.org.objectweb.asm.MethodVisitor;
import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.ProtectionDomain;
import static jdk.internal.org.objectweb.asm.Opcodes.*;
/** * Transformer.class - By 「Java者说」 -> https://www.jiweichengzhu.com/ * Created by wolffy on 2022/2/15. */public class HelloTransformer implements ClassFileTransformer {
/** * 转换提供的类文件并返回一个新的替换类文件 * * @param loader 要转换的类的定义加载器,如果引导加载器可能为null * @param className Java 虚拟机规范中定义的完全限定类和接口名称的内部形式的类名称。例如, "java/util/List" 。 * @param classBeingRedefined 如果这是由重新定义或重新转换触发的,则该类被重新定义或重新转换;如果这是一个类加载, null * @param protectionDomain 被定义或重新定义的类的保护域 * @param classfileBuffer 类文件格式的输入字节缓冲区 - 不得修改 * @return 转换后的字节码数组 * @throws IllegalClassFormatException class文件格式异常 */ @Override public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// 加入了自定义的transformer之后,所有需要加载的、但是还没有加载的类,当它们每一个要加载的时候,就需要通过transform方法 // 所以需要加一个判断,只处理AgentTest类 if (className.equals("com/wolffy/hello/test/AgentTest")) { System.out.println("<----------------- agent加载生效,开始更改class字节码 ----------------->"); return dumpAgentTest(); }
// 其他AgentTest之外的类,我们直接返回null,就代表没有任何修改,还是用它原来的class // return new byte[0]; return null; }
/** * 更改AgentTest的字节码,将system.out打印的内容给替换掉 *

* 设计到asm更改字节码的知识点,网络上很少有一个完整的知识体系,偶尔找到两篇文章也还都是抄来抄去,这里给大家提供一个学习的地址:https://lsieun.github.io/java/asm/index.html * * @return 更改之后的class文件字节流 */ private static byte[] dumpAgentTest() { ClassWriter cw = new ClassWriter(0); MethodVisitor mv;
cw.visit(52, ACC_PUBLIC + ACC_SUPER, "com/wolffy/hello/test/AgentTest", null, "java/lang/Object", null);
{ mv = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false); mv.visitInsn(RETURN); mv.visitMaxs(1, 1); mv.visitEnd(); }
{ mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); mv.visitCode(); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("hello world - https://www.jiweichengzhu.com/"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitInsn(RETURN); mv.visitMaxs(2, 1); mv.visitEnd(); }
cw.visitEnd();
return cw.toByteArray(); }
}


然后,我们在 premain 中将其引入,这样它才会生效,我们才能通过它修改 class 文件。


package com.wolffy.hello;
import java.lang.instrument.Instrumentation;
/** * 预先处理,程序启动时优先加载,JavaAgent.class - By 「Java者说」 -> https://www.jiweichengzhu.com/ * Created by wolffy on 2022/2/15. */public class HelloPremain {
public static void premain(String agentArgs, Instrumentation inst) { System.out.println("Yes, I am a real Agent for premain Class.");
// 引入自定义的transformer inst.addTransformer(new HelloTransformer(), true); }
}


引入完毕,我们直接用上面讲过的 premain 方式来运行一下看看效果如何。

直接打两个 jar 包:HelloAgent.jar、HelloTest.jar,然后用 java -jar 命令运行。



如上图,HelloTest 最终输出的文案,已经不是我在代码中写的那一段了,代码中写的是“hello, here is hello test for agent.”,而最终打印出来的是“hello world - https://www.jiweichengzhu.com/”。

over,演示完毕,大家是不是觉着这东西很神奇,同时也很可怕,这都不是在改你的 java 代码了,直接在运行之前把你 class 字节码给你改了!!!



案例代码:
https://github.com/yuluowuying/JavaAgentDemo
https://gitee.com/feisong/java-agent-demo



扫描二维码获取

更多精彩

Java者说




7
字节码框架学习




上面通过一个非常简单的 demo 给大家演示了如何去修改字节码,以及修改字节码都能够达到的效果。

这里给大家推荐几个市面上常见的几款字节码工具,通过它们封装的一些 API 也可以进行字节码操作。

BCEL:Byte Code Engineering Library,这是Apache Software Foundation 的Jakarta 项目的一部分,它可以让您深入 JVM 汇编语言进行类操作的细节。

JBET:Java Binary Enhancement Tool,用一种结构化的方式来展现Javabinary (.class)文件的内容,并且可以很容易的进行修改,可对Class文件进行分解,重新组合。

Javassist:Javassist 是一个开源的分析、编辑和创建 Java 字节码的类库,已加入了开放源代码 JBoss 应用服务器项目,通过使用 Javassist 对字节码操作为 JBoss 实现动态 AOP 框架。

ASM:是一个强大的、高性能、高质量的 Code 生成类库,它可以在运行期扩展Java类与实现Java接口,cglib封装了asm,可以在运行期动态生成新的 class,Hibernate和Spring都用到过它,cglib用于AOP,jdk中的proxy必须基于接口,cglib却没有这个限制。

bcel 和 jbet 我没怎么用过,了解不多,javassist 和 asm 我都有亲自使用过,二者虽然功能类似,但还是有不少区别的,从我个人使用角度来给大家总结几点差异。

1、javassist 提供的是基于源码级别的 API,而 asm 则是基于字节码层面的 API;
2、javassist 使用起来更加简单,而 asm 则要求有字节码方面的知识;
3、asm 直接操作字节码,更加贴近底层,灵活度方面要高于 javassist;
4、操作源码比操作字节码多了一层,所以 asm 的性能要高于 javassist;

以上是从它们的技术实现上手难易度这两个层面做的区分,其他层面的差异也有,这里不再细说。

就我个人使用感受来说,还是 asm 来的实在,因为我们一般都是用 agent 技术来寄生于其他应用之上,那么 agent 的性能就是我们的首要考虑因素,再怎么搞也不能影响到原来的应用,其次我们既然是要操作底层字节码,那除了性能之外,第二考虑的应该就是灵活度,越灵活的东西,我们能用来实现的功能就越多。

但是字节码这个东西,可以说是一个冷门技术,日常的工作中很少会直接使用它,所以网络上找的一些资料也都不是很齐全,甚至有的直接就是翻译官方文档,非常的生硬,生搬硬套很不容易理解。

在这里推荐给大家一个精讲 asm 技术的网站:https://lsieun.github.io/,站长 lsieun 是研究生毕业,从事 java 底层技术研究多年,对 asm 技术有很深入的理解,写的一些文章中都配有 demo 讲解,通俗易懂,大家有兴趣的可以去逛逛。



由于 asm 技术偏底层,入门有一定的门槛,大家直接看文章可能有难度,为此站长  lsieun 特意录制了视频教程,若有需要,大家可以去看一下,因为付出了不少时间和精力,所以收取一定的费用,价格不贵,绝对物超所值。

如果有想要报名学习的,不要着急过去,先从下面的链接中领取一张免费的优惠券,希望你们通过这套课程进行系统的学习之后,能够彻底掌握 asm 这个框架



课程报名地址:
https://ke.qq.com/course/4335150

优惠领取地址:
https://lsieun.github.io/java-agent/dream



1. 5 道面试题,拿捏 String 底层原理!

2. JMH 和 Arthas 定位问题的案例分享 !

3. 增加了一行代码,让我们提高了 3000% 的性能

4. private修饰的方法可以通过反射访问,那么private的意义是什么?

最近面试BAT,整理一份面试资料Java面试BATJ通关手册,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。

获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。

PS:因公众号平台更改了推送规则,如果不想错过内容,记得读完点一下在看,加个星标,这样每次新文章推送才会第一时间出现在你的订阅列表里。

“在看”支持小哈呀,谢谢啦😀

浏览 25
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报