相关背景及资源:html
曹工说Spring Boot源码(1)-- Bean Definition究竟是什么,附spring思惟导图分享java
曹工说Spring Boot源码(2)-- Bean Definition究竟是什么,我们对着接口,逐个方法讲解git
曹工说Spring Boot源码(3)-- 手动注册Bean Definition不比游戏好玩吗,咱们来试一下web
曹工说Spring Boot源码(4)-- 我是怎么自定义ApplicationContext,从json文件读取bean definition的?spring
曹工说Spring Boot源码(5)-- 怎么从properties文件读取beanshell
曹工说Spring Boot源码(6)-- Spring怎么从xml文件里解析bean的apache
曹工说Spring Boot源码(7)-- Spring解析xml文件,到底从中获得了什么(上)json
曹工说Spring Boot源码(8)-- Spring解析xml文件,到底从中获得了什么(util命名空间)api
曹工说Spring Boot源码(9)-- Spring解析xml文件,到底从中获得了什么(context命名空间上)数组
曹工说Spring Boot源码(10)-- Spring解析xml文件,到底从中获得了什么(context:annotation-config 解析)
曹工说Spring Boot源码(11)-- context:component-scan,你真的会用吗(此次来讲说它的奇技淫巧)
曹工说Spring Boot源码(12)-- Spring解析xml文件,到底从中获得了什么(context:component-scan完整解析)
曹工说Spring Boot源码(13)-- AspectJ的运行时织入(Load-Time-Weaving),基本内容是讲清楚了(附源码)
曹工说Spring Boot源码(14)-- AspectJ的Load-Time-Weaving的两种实现方式细细讲解,以及怎么和Spring Instrumentation集成
曹工说Spring Boot源码(15)-- Spring从xml文件里到底获得了什么(context:load-time-weaver 完整解析)
曹工说Spring Boot源码(16)-- Spring从xml文件里到底获得了什么(aop:config完整解析【上】)
曹工说Spring Boot源码(17)-- Spring从xml文件里到底获得了什么(aop:config完整解析【中】)
曹工说Spring Boot源码(18)-- Spring AOP源码分析三部曲,终于快讲完了 (aop:config完整解析【下】)
曹工说Spring Boot源码(19)-- Spring 带给咱们的工具利器,建立代理不用愁(ProxyFactory)
曹工说Spring Boot源码(20)-- 码网恢恢,疏而不漏,如何记录Spring RedisTemplate每次操做日志
曹工说Spring Boot源码(21)-- 为了让你们理解Spring Aop利器ProxyFactory,我已经拼了
曹工说Spring Boot源码(22)-- 你说我Spring Aop依赖AspectJ,我依赖它什么了
曹工说Spring Boot源码(23)-- ASM又立功了,Spring原来是这么递归获取注解的元注解的
曹工说Spring Boot源码(24)-- Spring注解扫描的瑞士军刀,asm技术实战(上)
工程结构图:
上一篇,咱们讲了ASM基本的使用方法,具体包括:复制一个class、修改class版本号、增长一个field、去掉一个field/method等等;同时,咱们也知道了怎么才能生成一个全新的class。
可是,仅凭这点粗浅的知识,咱们依然不太理解能干吗,本篇会带你们实现简单的AOP功能,固然了,学完了以后,可能你像我同样,更困惑了,那说明你变强了。
本篇的核心是,在JVM加载class的时候,去修改class,修改class的时候,加入咱们的aop逻辑。JVM加载class的时候,去修改class,这项技术就是load-time-weaver,实现load-time-weaver有两种方式,这两种方式,核心差异在于修改class的时机不一样。
在直接开始前,声明本篇文章,是基于下面这篇文章中的代码demo,我本身稍作了修改,并附上源码(原文是贴了代码,可是没有直接提供代码地址,不贴心啊)。
目标就是给下面的测试类,加上一点点切面功能。
package org.xunche.app; public class HelloXunChe { public static void main(String[] args) throws InterruptedException { HelloXunChe helloXunChe = new HelloXunChe(); helloXunChe.sayHi(); } public void sayHi() throws InterruptedException { System.out.println("hi, xunche"); sleep(); } public void sleep() throws InterruptedException { Thread.sleep((long) (Math.random() * 200)); } }
咱们但愿,class在执行的时候,可以打印方法执行的耗时,也就是,最终的class,须要是下面这样的。
package org.xunche.app; import org.xunche.agent.TimeHolder; public class HelloXunChe { public HelloXunChe() { } public static void main(String[] args) throws InterruptedException { TimeHolder.start(args.getClass().getName() + "." + "main"); // 业务逻辑开始 HelloXunChe helloXunChe = new HelloXunChe(); helloXunChe.sayHi(); //业务逻辑结束 HelloXunChe helloXunChe = args.getClass().getName() + "." + "main"; System.out.println(helloXunChe + ": " + TimeHolder.cost(helloXunChe)); } public void sayHi() throws InterruptedException { TimeHolder.start(this.getClass().getName() + "." + "sayHi"); System.out.println("hi, xunche"); // 业务逻辑开始 this.sleep(); //业务逻辑结束 String var1 = this.getClass().getName() + "." + "sayHi"; System.out.println(var1 + ": " + TimeHolder.cost(var1)); } public void sleep() throws InterruptedException { TimeHolder.start(this.getClass().getName() + "." + "sleep"); // 业务逻辑开始 Thread.sleep((long)(Math.random() * 200.0D)); //业务逻辑结束 String var1 = this.getClass().getName() + "." + "sleep"; System.out.println(var1 + ": " + TimeHolder.cost(var1)); } }
因此,咱们大概就是,要作下面的这样一个切面:
@Override protected void onMethodEnter() { //在方法入口处植入 String className = getClass().getName(); String s = className + "." + methodName; TimeHolder.start(s); } @Override protected void onMethodExit(int i) { //在方法出口植入 String className = getClass().getName(); String s = className + "." + methodName; long cost = TimeHolder.cost(s); System.out.println(s + ": " + cost); }
可是,习惯了动态代理的咱们,看上面的代码可能会有点误解。上面的代码,不是在执行目标方法前,调用切面;而是:直接把切面代码嵌入了目标方法。
想必你们都明确了要达成的目标了,下面说,怎么作。
这部分,你们能够结合开头那个连接一块儿学习。
首先,我请你们看看java命令行的选项。直接在cmd里敲java,出现以下:
看了和没看同样,那咱们再看一张图,在你们破解某些java编写的软件时,可能会涉及到jar包破解,好比:
你们可使用jad这类反编译软件,打开jar包看下,看看里面是啥:
能够发现,里面有一个MANIFEST.MF文件,里面指定了Premain-Class这个key-value,从这个名字,你们可能知道了,咱们平时运行java程序,都是运行main方法,这里来个premain,那这意思,就是在main方法前面插个队呗?
你说的没有错,确实是插队了,拿上面的破解jar包举例,里面的Premain-Class方法,对应的Agent类,反编译后的代码以下:
核心代码就是图里那一行:
java.lang.instrument.Instrumentation public interface Instrumentation { /** * Registers the supplied transformer. All future class definitions * will be seen by the transformer, except definitions of classes upon which any * registered transformer is dependent. * The transformer is called when classes are loaded, when they are * {@linkplain #redefineClasses redefined}. and if <code>canRetransform</code> is true, * when they are {@linkplain #retransformClasses retransformed}. * See {@link java.lang.instrument.ClassFileTransformer#transform * ClassFileTransformer.transform} for the order * of transform calls. * If a transformer throws * an exception during execution, the JVM will still call the other registered * transformers in order. The same transformer may be added more than once, * but it is strongly discouraged -- avoid this by creating a new instance of * transformer class. * <P> * This method is intended for use in instrumentation, as described in the * {@linkplain Instrumentation class specification}. * * @param transformer the transformer to register * @param canRetransform can this transformer's transformations be retransformed * @throws java.lang.NullPointerException if passed a <code>null</code> transformer * @throws java.lang.UnsupportedOperationException if <code>canRetransform</code> * is true and the current configuration of the JVM does not allow * retransformation ({@link #isRetransformClassesSupported} is false) * @since 1.6 */ void addTransformer(ClassFileTransformer transformer, boolean canRetransform); ... }
这个类,就是官方jdk提供的类,官方的本意呢,确定是让你们,在加载class的时候,给你们提供一个机会,去修改class,好比,某个第三方jar包,咱们须要修改,可是没有源码,就能够这么干;或者是一些要统一处理,不方便在应用中耦合的功能:好比埋点、性能监控、日志记录、安全监测等。
说回这个方法,参数为ClassFileTransformer,这个接口,就一个方法,你们看看注释:
/** * ... * * @param classfileBuffer the input byte buffer in class file format - must not be modified * * @throws IllegalClassFormatException if the input does not represent a well-formed class file * @return a well-formed class file buffer (the result of the transform), or <code>null</code> if no transform is performed. * @see Instrumentation#redefineClasses */ byte[] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException;
别的也很少说了,反正就是:jvm给你原始class,你本身修改,还jvm一个改后的class。
因此,你们估计也能猜到破解的原理了,但我仍是但愿你们:有能力支持正版的话,仍是要支持。
接下来,咱们回到咱们的目标的实现上。
完整代码:https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/java-agent-premain-demo
package org.xunche.agent; import org.objectweb.asm.*; import org.objectweb.asm.commons.AdviceAdapter; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; public class TimeAgentByJava { public static void premain(String args, Instrumentation instrumentation) { instrumentation.addTransformer(new TimeClassFileTransformer()); } }
类转换器的详细代码以下:
private static class TimeClassFileTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { if (className.startsWith("java") || className.startsWith("jdk") || className.startsWith("javax") || className.startsWith("sun") || className.startsWith("com/sun")|| className.startsWith("org/xunche/agent")) { //return null或者执行异常会执行原来的字节码 return null; } // 1 System.out.println("loaded class: " + className); ClassReader reader = new ClassReader(classfileBuffer); // 2 ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); // 3 reader.accept(new TimeClassVisitor(writer), ClassReader.EXPAND_FRAMES); // 4 return writer.toByteArray(); } }
1处,将原始的类字节码加载到classReader中
ClassReader reader = new ClassReader(classfileBuffer);
2处,将reader传给ClassWriter,这个咱们没讲过,大概就是使用classreader中的东西,来构造ClassWriter;能够差很少理解为复制classreader的东西到ClassWriter中。
你们能够看以下代码:
public ClassWriter(final ClassReader classReader, final int flags) { super(Opcodes.ASM6); symbolTable = new SymbolTable(this, classReader); ... }
这里new了一个对象,SymbolTable。
SymbolTable(final ClassWriter classWriter, final ClassReader classReader) { this.classWriter = classWriter; this.sourceClassReader = classReader; // Copy the constant pool binary content. byte[] inputBytes = classReader.b; int constantPoolOffset = classReader.getItem(1) - 1; int constantPoolLength = classReader.header - constantPoolOffset; constantPoolCount = classReader.getItemCount(); constantPool = new ByteVector(constantPoolLength); constantPool.putByteArray(inputBytes, constantPoolOffset, constantPoolLength); ... }
你们直接看上面的注释吧,Copy the constant pool binary content
。反正吧,基本能够理解为,classwriter拷贝了classreader中的一部分东西,应该不是所有。
为何不是所有,由于我试了下:
public static void main(String[] args) throws IOException { ClassReader reader = new ClassReader("org.xunche.app.HelloXunChe"); ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); byte[] bytes = writer.toByteArray(); File file = new File( "F:\\gitee-ckl\\all-simple-demo-in-work\\java-agent-premain-demo\\test-agent\\src\\main\\java\\org\\xunche\\app\\HelloXunChe.class"); FileOutputStream fos = new FileOutputStream(file); fos.write(bytes); fos.close(); }
上面这样,出来的class文件,是破损的,格式不正确的,没法反编译。
3处,使用TimeClassVisitor做为writer的中间商,此时,顺序变成了:
classreader --> TimeClassVisitor --> classWriter
4处,返回writer的字节码,给jvm;jvm使用该字节码,去redefine一个class出来
public static class TimeClassVisitor extends ClassVisitor { public TimeClassVisitor(ClassVisitor classVisitor) { super(Opcodes.ASM6, classVisitor); } // 1 @Override public MethodVisitor visitMethod(int methodAccess, String methodName, String methodDesc, String signature, String[] exceptions) { MethodVisitor methodVisitor = cv.visitMethod(methodAccess, methodName, methodDesc, signature, exceptions); // 2 return new TimeAdviceAdapter(Opcodes.ASM6, methodVisitor, methodAccess, methodName, methodDesc); } } }
咱们这里的TimeAdviceAdapter,主要是但愿在方法执行先后作点事,相似于切面,因此继承了一个AdviceAdapter,这个AdviceAdaper,帮咱们实现了MethodVisitor的所有方法,咱们只须要覆写咱们想要覆盖的方法便可。
好比,AdviceAdaper,由于继承了MethodVisitor,其visitCode方法,会在访问方法体时被回调:
@Override public void visitCode() { super.visitCode(); // 1 onMethodEnter(); } //2 protected void onMethodEnter() {}
因此,咱们最终的TimeAdviceAdaper,代码以下:
public static class TimeAdviceAdapter extends AdviceAdapter { private String methodName; protected TimeAdviceAdapter(int api, MethodVisitor methodVisitor, int methodAccess, String methodName, String methodDesc) { super(api, methodVisitor, methodAccess, methodName, methodDesc); this.methodName = methodName; } @Override protected void onMethodEnter() { //在方法入口处植入 if ("<init>".equals(methodName)|| "<clinit>".equals(methodName)) { return; } String className = getClass().getName(); String s = className + "." + methodName; TimeHolder.start(s); } @Override protected void onMethodExit(int i) { //在方法出口植入 if ("<init>".equals(methodName) || "<clinit>".equals(methodName)) { return; } String className = getClass().getName(); String s = className + "." + methodName; long cost = TimeHolder.cost(s); System.out.println(s + ": " + cost); } }
这份代码看着可还行?惋惜啊,是假的,是错误的!写asm这么简单的话,那我要从梦里笑醒。
为啥是假的,由于:真正的代码,是长下面这样的:
看到这里,是否是想溜了,这都啥玩意,看不懂啊,不过不要着急,办法总比困难多。
咱们先装个idea插件,叫:asm-bytecode-outline
。这个插件的做用,简而言之,就是帮你把java代码翻译成ASM的写法。在线装不了的,能够离线装:
装好插件后,只要在咱们的TimeAdviceAdapter类,点右键:
就会生成咱们须要的ASM代码,而后拷贝:
何时拷贝结束呢?
基本上,这样就能够了。
做为一个常年掉坑的人,我在这个坑里也摸爬了整整一天。
你们能够看到,咱们的java写的方法里,是这样的:
@Override protected void onMethodEnter() { //在方法入口处植入 if ("<init>".equals(methodName)|| "<clinit>".equals(methodName)) { return; } String className = getClass().getName(); // 1. String s = className + "." + methodName; TimeHolder.start(s); }
因此,asm也帮咱们贴心地生成了这样的语句:
mv.visitFieldInsn(Opcodes.GETFIELD, "org/xunche/agent/TimeAgentByJava$TimeAdviceAdapter", "methodName", "Ljava/lang/String;");
看起来就像是说,访问org/xunche/agent/TimeAgentByJava$TimeAdviceAdapter类的methodName字段。
可是,这是有问题的。由于,这段代码,最终aop切面会被插入到target:
public class HelloXunChe { private String methodName = "abc"; public static void main(String[] args) throws InterruptedException { HelloXunChe helloXunChe = new HelloXunChe(); helloXunChe.sayHi(); } public void sayHi() throws InterruptedException { System.out.println("hi, xunche"); sleep(); } public void sleep() throws InterruptedException { Thread.sleep((long) (Math.random() * 200)); } }
我实话跟你说,这个target类里,压根访问不到org/xunche/agent/TimeAgentByJava$TimeAdviceAdapter类的methodName字段。
我是怎么发现这个问题的,以前一直报错,直到我在target后来加了这么一行:
public class HelloXunChe { private String methodName = "abc"; ... }
哎,没个大佬带我,真的难。
固然,我是经过这个确认了上述问题,最终解决的思路呢,就是:把你生成的class,反编译出来看看,看看是否是你想要的。
因此,我专门写了个main测试类,来测试改后的class是否符合预期。
public class SaveGeneratedClassWithOriginAgentTest { public static void main(String[] args) throws IOException { //1 ClassReader reader = new ClassReader("org.xunche.app.HelloXunChe"); ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); reader.accept(new TimeAgentByJava.TimeClassVisitor(writer), ClassReader.EXPAND_FRAMES); byte[] bytes = writer.toByteArray(); // 2 File file = new File( "F:\\ownprojects\\all-simple-demo-in-work\\java-agent-premain-demo\\test-agent\\src\\main\\java\\org\\xunche\\app\\HelloXunCheCopy2.class"); FileOutputStream fos = new FileOutputStream(file); fos.write(bytes); fos.close(); } }
因此,上面那段asm,你们若是看:
会发现,访问methodname那句代码,是这么写的:
mv.visitLdcInsn(methodName);
这就是,至关于直接把methodName写死到最终的class里去了;最终的class就会是想要的样子:
public void sayHi() throws InterruptedException { //1 TimeHolder.start(this.getClass().getName() + "." + "sayHi"); System.out.println("hi, xunche"); this.sleep(); // 2 String var1 = this.getClass().getName() + "." + "sayHi"; System.out.println(var1 + ": " + TimeHolder.cost(var1)); }
插件中,配置Premain-Class
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.3.1</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Premain-Class> org.xunche.agent.TimeAgent </Premain-Class> </manifestEntries> </archive> </configuration> </plugin>
测试模块,没啥开发的,就只有那个target那个类。
最终我是这么运行的:
java -javaagent:agent.jar -classpath lib/*;java-agent-premain-demo.jar org/xunche/app/He lloXunChe
这里指定了lib目录,主要是agent模块须要的jar包:
简单的运行效果以下:
loaded class: org/xunche/app/HelloXunChe methodName = 0 <init> methodName = 0 main methodName = 0 sayHi methodName = 0 sleep hi, xunche org.xunche.app.HelloXunChe.abc: 129 org.xunche.app.HelloXunChe.abc: 129
ASM这个东西,想要不熟悉字节码就去像我上面这样傻瓜操做,坑仍是比较多的,比较难趟。回头有空再介绍字节码吧。我也是半桶水,你们一块儿学习吧。
本节源码:
https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/java-agent-premain-demo