给大家送上下面的java学习资料java对象行为java.lang.instrument.Instrumentation直接操作字节码程序有问题,一时看不出问题出在哪里,于是有了以下对话:“调试它。”“在线机,Debug口没有打开。”“看日志看请求值和返回值是什么?”“那段代码没有打印日志。”“更改代码,添加日志,然后重新发布。”“怀疑是线程池的问题,重启会破坏站点。”几十秒的沉默后:“据说最高级别的故障排查,只有通过Reviewcode才能发现问题。”数十秒以上的沉默后:“轮询了17次代码后,我终于有了结论。”“结论是?”“我还没有达到只能通过Review代码才能发现问题的最高境界。”Java对象行为一文开头的问题本质上是一个动态改变内存中已有对象行为的问题。因此,首先要搞清楚JVM与对象的行为有什么关系,是否有改变的可能。对象使用两种东西来描述事物:行为和属性。例如:publicclassPerson{privateintage;私有字符串名称;publicvoidspeak(Stringstr){System.out.println(str);}publicPerson(intage,Stringname){this.age=age;这。名字=名字;}}在上面的Person类中,age和name是属性,speak是行为。对象是类的实例,每个对象的属性都属于对象本身,但每个对象的行为都是公开的。比如我们现在基于Person类创建两个对象personA和personB:PersonpersonA=newPerson(43,"lixunhuan");personA.speak("我是李寻欢");PersonpersonB=newPerson(23,"afei");personB.speak("我是阿飞");personA和personB有自己的名字和年龄,但是他们有一个共同的行为:说话。想象一下,如果我们是Java语言的设计者,我们将如何存储对象的行为和属性?“很简单,属性跟随对象,每个对象都有一个副本。行为是公共的东西,抽取出来放在单独的地方。”“嗯?提取公共部分类似于代码重用。”““路越简单越好,很多事情都是一个目标。”也就是说,第一步就是要找到这个公共的地方来存放对象behavior.经过一番搜索,我们发现了这样的描述:方法区是在虚拟机启动时创建的,在所有Java虚拟机线程之间共享,它在逻辑上是堆区的一部分。它存储了每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码。Java对象的行为(方法、函数)都存放在方法区。“方法区的数据从哪里来?”“方法区的数据是加载类时从class文件中提取。”“class文件从哪里来?”“从Java或者其他符合JVM规范的源代码编译而来。”“源代码从哪里来?”“废话,当然是手写了!”“往后推,手写没问题,编译没问题,至于加载……有没有办法加载一个已经加载过的类?如果有,我们可以在字节码中修改目标方法所在的区域,然后重新加载类,这样在不改变对象的属性或影响的情况下,改变方法区中的对象行为(方法)对象的状态已经存在,那么这个问题就可以解决了。但是,这不是违反了JVM的类加载原则吗?毕竟,我们不想更改ClassLoader。“年轻人,你可以去看看java.lang.instrument.Instrumentation。”"java.lang.instrument.Instrumentation看了文档,发现有两个接口:redefineClasses和retransformClasses。一个是重定义类,一个是修改类。这两个类似,看redefineClasses的说明:这个方法用于在不引用现有类文件字节的情况下替换类的定义,就像从源代码重新编译以进行修复并继续调试时可能会做的那样。要转换现有类文件字节的地方(例如在字节码中instrumentation)retransformClasses应该是用retransformClasses来替换已有的class文件,redefineClasses是提供字节码文件来替换已有的class文件,retransformClasses是修改已有的字节码文件然后替换,当然runtime直接替换一个类是不安全的,比如新建一个类文件引用了一个不存在的类,或者删除了某个类的字段等等,都会引发异常。所以如文档中所述,对instrument有很多限制:重定义可能会改变方法体、常量池和属性。重新定义不得添加、删除或重命名字段或方法,不得更改方法的签名或更改继承。这些限制可能会在未来的版本中取消。在应用转换之前,不会检查、验证和安装类文件字节,如果生成的字节有误,此方法将抛出异常。基本上我们能做的就是简单的修改方法中的一些行为,这对于我们一开始的问题来说已经足够了,打印一条日志。当然,除了通过retransform打印日志,我们还可以做很多其他非常有用的事情,下面会介绍。那么如何获取我们需要的类文件呢?最简单的方法之一是重新编译修改后的Java文件得到类文件,然后调用redefineClasses替换它。但是对于没有(或无法获取,或不便修改)源代码的文件,我们应该怎么办呢?其实对于JVM来说,不管是Java还是Scala,任何符合JVM规范的语言的源代码都可以编译成class文件。JVM运行的对象是类文件,而不是源代码。所以,从这个意义上说,我们可以说“JVM与语言无关”。既然如此,不管有没有源码,我们只需要修改class文件即可。直接运行字节码Java是软件开发者可以理解的语言,类字节码是JVM可以理解的语言,类字节码最终会被JVM解释成机器可以理解的语言。所有的语言都是人类创造的。因此,理论上(事实上也是如此)人们可以理解上述任何一种语言,既然能够理解,自然就可以对其进行修改。只要我们愿意,我们可以跳过Java编译器,直接写字节码文件,但这不符合时代的发展。高级语言设计毕竟是为我们人类服务的,其开发效率也高于机器。语言要高得多。对于人类来说,字节码文件的可读性远不如Java代码。尽管如此,一些优秀的程序员已经创建了一个可以用来直接编辑字节码的框架,提供了一个接口让我们可以轻松地操作字节码文件、注入方法修改类、动态创建新类等等。其中最著名的框架应该就是ASM了。cglib、Spring等框架中字节码的运行都是基于ASM的。我们都知道Spring的AOP是基于动态代理实现的。Spring会在运行时动态创建一个代理类。代理类是指代理类,在代理方法执行前后进行一些神秘的操作。那么,Spring是如何在运行时创建代理类的呢?动态代理的美妙之处在于我们不必为每个需要代理的类手动编写代理类代码。Spring会在运行时根据需要动态创建一个类。这里创建的过程并不是通过字符串写Java文件,然后编译成class文件,然后加载。Spring会直接“创建”一个类文件,然后加载它。创建类文件的工具是ASM。至此,我们知道可以使用ASM框架直接操作class文件,在类中添加一段代码打印日志,然后重新改造。BTrace到现在为止,我们都停留在理论描述的层面。那么如何实施呢?我们先来看几个问题:在我们的项目中,查找字节码,修改字节码,然后重新转化的动作由谁来做?我们不是预言家,以后会不会遇到本文开头的问题就不得而知了。考虑到成本效益,我们不可能在每个项目中都开发一段专门用于修改字节码和重新加载字节码的代码。如果JVM不是本地的而是远程的怎么办?如果您甚至不能使用ASM怎么办?能不能再笼统一点,再“傻一点”一点。幸运的是,因为BTrace的存在,我们不用自己写一套这样的工具。什么是BTrace?BTrace已经开源,项目描述极其简短:Asafe,dynamictracingtoolfortheJavaplatform。BTrace是一种基于Java语言的安全、动态的跟踪工具。BTrace是基于ASM、JavaAttachAPI和Instrument开发的,为用户提供了很多注解。依靠这些注解,我们可以编写BTrace脚本(简单的Java代码)来实现我们想要的,而不会卡在ASM对字节码的操作中。看BTrace官方提供的一个简单例子:拦截所有java.io包中所有类中以read开头的方法,打印类名、方法名和参数名。当程序的IO负载比较高时,可以从输出信息中看出是哪些类引起的。是不是很方便?packagecom.sun.btrace.samples;importcom.sun.btrace.annotations.*;importcom.sun.btrace.AnyType;importstaticcom.sun.btrace.BTraceUtils.*;/***此示例演示正则表达式*探测匹配并获取输入参数*作为一个数组——这样任何重载变体*都可以在“一个地方”进行追踪。此示例*跟踪*java.io包中任何类的任何“readXX”方法。被探测的类、方法和arg*数组打印在操作中。*/@BTracepublicclassArgArray{@OnMethod(clazz="/java\\.io\\..*/",method="/read.*/")publicstaticvoidanyRead(@ProbeClassNameStringpcn,@ProbeMethodNameStringpmn,AnyType[]args){println(pcn);打印(下午);打印数组(参数);}}再看一个例子:每2秒打印一次,直到当前创建的Threads。packagecom.sun.btrace.samples;importcom.sun.btrace.annotations.*;importstaticcom.sun.btrace.BTraceUtils.*;importcom.sun.btrace.annotations.Export;/***此示例创建了一个每次调用Thread.start()时,jvmstat计数器和*递增它。可以从进程外部访问*这个线程数。@Export注释的*字段映射到jvmstat计数器。计数器*名称是“btrace”。+<类名>+"."+
