ASM

ASM是个2002年就开始的项目,真是古老至极。许多流行且包含了动态代理功能的框架应该直接或者间接使用了asm(cglib是基于asm做的二次开发)。

在最近的项目中使用到了asm,所以做了一些小记,内容有翻译,有摘录,有总结。

ASM 模型

ASM提供了两套API:

  • core API : 提供 event-based 字节码控制。

    asm通过JVMS定义的字节码结构把字节码中的如field,method 声明,指令等变成event,然后操纵event以达成修改字节码的目的。

    相关主要类:ClassVisitor, MethodVisitor (4.0之前还有基于这两个类的如 ClassAdapter 类等)

  • tree API : 提供 tree-based 字节码控制

    基于event-based的模型,继续抽象了下,用一棵树的形式展现字节码。

    相关主要类:ClassNode, MethodNode

此文中就拿了SAX 和 DOM 来描述解析xml的比较 来 类比 core API 和 tree API 的比较:

  • core API 比 tree API 占用更少的系统资源。从内存的角度看:tree API 由于要把字节码抽象成tree,在内存中会占用跟多的空间

  • core API 比 tree API 更难用,每次只能操作一个指令,需要非常了解字节码相关规范,写起来要小心翼翼。

ASM 架构

经典图如下:
经典图

使用了经典的 producer-consumer 模型,中间的白框都是filter。

producer --> filters --> consumer:

对于Class:

ClassReader 是 producer,filter 都是 ClassVisitor 的实现类,ClassWriter 作为 consumer。

对于Method:

ClassReader 是 producer, filter 是 MethodVistor 的实现类, MethodWriter 作为consumer。

对于Field:

ClassReader 是 producer, filter 是 FieldVistor 的实现类, FieldWriter 作为consumer。

ASM 实例

添加System.out.println(“here I am”); 代码到方法中:

  • core api使用方法如下:

    ClassReader cr = new ClassReader(bytes);
    ClassWriter cw = new ClassWriter(cr, COMPUTE_FRAMES | COMPUTE_MAXS); //cr只是定义来源reader,无用。
    ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, cw){
        ....
    }
    cr.accept(cv, ClassReader.EXPAND_FRAMES);                
    return cw.toByteArray();
    
    //cr-->cv-->cw
    
  • tree api使用方法如下:

    ClassReader cr = new ClassReader(bytecode);
    ClassNode cn = new ClassNode();
    cr.accept(cn, ClassReader.SKIP_DEBUG);
    
    List methods = cn.methods;
    for (int i = 0; i < methods.size(); ++i) {
        MethodNode method = (MethodNode) methods.get(i);
        if (method.instructions.size() > 0) {                   instructions.insert(new FieldInsnNode(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;")); 
            instructions.insert(new LdcInsnNode("hi, here I am!"));
            instructions.insert(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false))
        }
    }
    ClassWriter cw = new ClassWriter(COMPUTE_FRAMES | COMPUTE_MAXS);
    cn.accept(c);
    return cw.toByteArray();
    
    //cr-->cn-->cw
    

PS:推荐在IDE上面安装ASM字节码工具,在一个正常类里面写完代码以后,用工具一转,可以参考转出的字节码,开始写asm相关代码了。

ASM其他

ASM core api

  • TraceClassVisitor:

    通过 PrintWriter 实例打印可读的字节码信息。

  • CheckClassAdapter

    在被JVM拒绝之前检测修改后的字节码是否合法。

  • AnalyzerAdapter

    可以自动计算stack的深度,感觉没太大作用,在ClassWriter的option里面有两个选项:COMPUTE_FRAMES, COMPUTE_MAXS

  • LocalVariablesSorter

    有newLocal方法可以添加local variable,自动维护local variable数量变化,引起的local variable对应的index变化的问题。

  • AdviceAdapter

    有onMethodEnter()和onMethodExit()方法,可以在方法最开始和RETURN 或者 ATHROW指令之前插入指令。

Metadata 相关 vistor:SignatureVisitor, AnnotationVisitor。这两个还没有用过,暂做记录。

ASM tree api

实际使用的时候,碰到了问题,没有细看,暂做记录。

Zero Copy

xflush,metaq都适用了zero copy技术来提高性能,多次看到这个词,却一直没有理解透彻,于是找了一些资料了解。以下此技术点知识的翻译及汇总。

翻译自这里

到目前为止几乎所有的人都听说过linux下的所谓的zero-copy,但是我还是经常碰到不能完全理解这个内容的人。 基于此,我打算写一些文章深挖,剖析一下这个有用的功能。在这个文章里面,我们从user-mode(用户模式)应用这个角度来看下zero copy, 所以更详尽的kernel-level(内核)细节会被有意的忽略。

What is Zero-Copy?

为了更好的理解一个问题的解决方案,我们首先需要的是理解问题本身。让我们了解下网络服务器进程(?network server dæmon )为client端提供数据存储在文件中的服务的简单过程中包含了什么。一下是实例代码:

read(file, tmp_buf, len);

write(socket, tmp_buf, len);

看起来非常简单;你可能会想里面最多也就两个系统调用(system calls)。事实上,实时也就是如此。在这两次调用之后,数据(data)已经被复制了最少四次,同时执行了相同次数的 user/kernel mode上下文切换 。(事实上这个过程比这个描述的更复杂)。为了更深入的理解这个过程,请看图1。上半部展示了user/kernel mode上下文切换,并且下半部展示了复制操作。

Copying in Two Sample System Calls

  • 第一步:读系统调用(read system call)引起了从user mode to kernel mode 的上下文切换。第一次的copy被DMA engine执行,它从磁盘中读取了文件的内容然后存储到kernel address space buffer 里面。

  • 第二步:第二次的copy是数据从kernel buffer 复制到 user buffer,然后读系统调用(read system call)返回。这个 返回 引起了从 kernel mode 变回 user mode 的上下文切换。现在数据被存在了user address space buffer了。

  • 第三步:写系统调用(write system call)引起了从user mode to kernel mode。第三次copy是为了把再次把数据放入到kernel address space buffer。这次数据被放在了不同的buffer,是跟sockets相关的buffer。

  • 第四步:写系统调用(write system call)返回,创建了我们的第四次的上下文切换。异步且于之前不依赖(Independently and asynchronously)的情况下,第四次是在DMA engine 从kernel buffer 复制 到 protocol engine的时候发生的。你可能问自己,“什么是 Independently and asynchronously? 数据难道不是在调用返回之前就完成数据传输的么?” 事实上,调用返回 步保证 输入传输完成;它甚至步保证数据开始传输。 它只是意味着网卡驱动在其处理队列中有清空的描述符(?free descriptors)并且接收了我们需要传输的数据。这个时候在网卡的处理队列中可能还有很多的数据包在我们的数据之前。除非驱动/硬件实现了优先级环活着队列,不然数据在处理队列中都是按照先进先出的方式传输的。(带分叉的DMA 复制 表示了最后的复制能够被延迟)。

正如你所看到的, 以上过程有很多的数据复制是不必要的。其中一部分复制是可以消除来减少资源浪费以提高性能的。 作为一个驱动开发者, 我在一些有非常先进功能的硬件上面工作过。有些硬件能够绕过主存直接传送数据到另外一个设备。这个功能能够减少系统内存中的数据冗余,非常值得拥有,但是不是所有的硬件都是支持的。可能还会存在额外的数据问题会引入一定的复杂性:来源于disk的数据需要repackage才能发送给网卡。 为了减少资源浪费, 我们可以从减少kernel buffers和 user buffers之间的数据复制。

减少一次复制的一种方法是跳过调用 read 而用 调用 mmap 来替代。举例来说:

tmp_buf = mmp(file, len);
write(socket, tmp_buf, len);

为了更好的了解这个过程,请看下图 2. Context switches(上下文切换)仍然一样。
Calling mmap

  • 第一步:mmap 系统调用通过DMA engine复制到kernel buffer。这个buffer之后会被user process(用户京城)共享使用,而不需要在kernel memory space 和 user memory sapce之间执行数据的复制。

  • 第二步:写系统调用让kernal 把之前的 kernel buffer中的数据传输到 socket相关的kernel buffer。

  • 第三步:第三次数据复制发生在 DMA engine 传输数据从 kernel socket buffers 到protocal engine。

通过使用mmap来替代read,我们已经减少了一半的kernel不得步复制的次数。在大量数据需要传输的时候,这种方式带来了很好的结果。然而,这种提升不是没有代价的;这种使用mmap+write方法的方式会存在隐藏的陷阱。其中一种情况是党你正在使用mmap(memory map)读取文件的时候并且已经调用了write操作 的时候,另外一个线程删减了文件的部分内容。你的write system call 就会被bus error(signal SIGBUS)打断,因为你执行了一个次有问题的内存访问。这个signal的默认行为就是杀死对应的进程和dump core--但对网络服务器无法记全所有希望了解的操作。有两种方式能够解决这个问题。

  • 第一种解决方案是为 SIGBUS signal 添加singal handler,然后handler只要简单的return逻辑就可以了。通过这种方式,write system call 会返回在它被中断前要写的字节流并且errno会被设置成success。这种方式在我看来是一种不好的解决方案,只是一种处理了表层症状而不是问题的根本的方式。因为SIGBUS signals 有时候说明了进程出现了非常严重的问题,但现在却被屏蔽了,所以不推荐使用这种方案。

  • 第二种解决方案使用了 kernel 的file leasing[文件租约](在Microsoft Windows里面叫做“opportunistic locking”)。我认为这是一种解决这个问题的正确方法。通过在文件描述符(file descriptor)续约(leasing),你可以在kernel中获得某个具体文件的租约(lease)。紧接着你可以在kernel中请求一次读/写租约(lease)。当另一个进程尝试着删减(修改)你正在传输的文件内容的时候,kernel会发送给你一个实时的signal,RT_SIGNAL_LEASE signal。它告诉你kernel正在打断你在某个文件上面的读/写租约。你的write system call会在你的程序访问非法地址和北SIGBUS signal杀掉之前就被中断。write system call 返回的数据是中断之间将要卸乳的字节流数据,并且errno 会被设置成 success。一下是从kernel获得租约的事例代码:

    if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
        perror("kernel lease set signal");
        return -1;
    }
    /* l_type can be F_RDLCK F_WRLCK */
    if(fcntl(fd, F_SETLEASE, l_type)){
        perror("kernel lease set type");
        return -1;
    }
    

    你需要在 mmap 文件之前获得租约,然后在你完成逻辑之后解除租约。解除租约可以通过调用 fcntl F_SETLEASE with the lease type of F_UNLCK 来完成。

在kerverl version 2.1, the sendfile system call 被添加来简化网络内机器之间或一台机器本地文件之间的数据传输。 sendfile 不但能减少数据的复制,还能减少context switches(kernel, user),是按照如下方式使用的:

sendfile(socket, file, len);

为了更好的理解这个过程,请看图3.
图3

  • 第一步:sendfile system call 把文件的数据通过DMA engine 复制到kernel buffer里面。然后数据通过kernel复制到sockets相关的kernel buffer里面。

  • 第二步:第三次的复制是当DMA engine 从kernel socket buffers中数据传送数据到protocal engine中。

你可能会想当另外一个线程删减这个我们正在使用sendfile system call 来传输的文件的时候会发生什么。如果我们没有注册任何signal handlers(信号量处理器),sendfile call 会返回被中断之前将要传输的字节流,并且errno会被设置为sucess状态。

如果我们在调用sendfile之前通过kernel获得获得文件的租约,那么之后的行为和返回状态就和之前描述的一样。我们也会在sendfile call 返回之前得到RT_SIGNAL_LEASE信号量。

到现在位置,我们已经能够减少若干次kernel发生的数据复制了,但是我们仍然留下了一次复制。那么是否那次复制也能避免?当然可以,只要通过一些硬件的帮助。为了消除所有通过kernel完成的数据复制,我们需要网卡支持gather(聚合) operations。简单理解就是等在等待被传输的数据不需要在连续的内存中;他们能够被存储在不同的内存位置。在内部版本2.4,socket buffer discriptor为了适应这些需求而做了修改--这就是Linux的zero copy。这个方法不但减少多次的context switches,而且它减少processors(处理器)完成的多次数据复制。对于user-level的应用什么都不需要改变,代码仍然如下:

sendfile(socket, file, len);

为了能够更深入的理解这个过程,请看图4.
图4

  • 第一步:the sendfile system call 通过DMA engine复制数据内容至kernel buffer。

  • 第二步:没有数据被复制到socket buffer。只有包含数据的 whereabouts(内存地址信息?)和长度信息的descriptors(描述符号)才被添加到socket buffer。The DMA engine 直接把数据从kernel buffer 传递到 protocol engine,所以消除了剩余最后的数据复制。

因为数据实际上仍然从disk复制到了memory并且之后从memory到了wire,有人可能会说这个不是真正的 zero copy。 之前所描述的是 操作系统角度 的zero copy,因为数据没有在kernel buffers之间被复制。当使用zero copy的时候,能得到除了避免数据复制之外的性能提升,例如更少的context switches,更少的CPU data cache pollution(cpu 数据缓存污染,不需要修改cpu的缓存内容?)和 no CPU checksum calculations(这个是什么?)

How to use Zero-Copy?

现在我们知道了什么是zero copy,让我们实践下理论并且写一些代码吧。

C的代码参考这里

以下为JAVA中如何完成zero copy,只要使用api:

java.nio.channel.FileChannel的transferTo(long position, long count, WritableByteChannel target) throws IOException;

来自参考的性能数据:

File size Normal file transfer (ms) transferTo (ms)
7MB 156 45
21MB 337 128
63MB 843 387
98MB 1320 617
200MB 2124 1150
350MB 3631 1762
700MB 13498 4422
1GB 18399 8537

内网穿透

内网穿透

机缘巧合,正好在考虑工作台可以进行远程控制的问题。打算平时工作就在位置上用台式工作台完成工作,如果是去短途会议或者出差,那么就带一个ipad就够了。

在这种思路下,考虑并且实践了一些方案。

Win

安装了win10,然后在ipad上面安装了remote desktop,非常非常好用;但是碰到一个问题,当自己拿着pad在家的时候,想要远程控制是不可能的了。于是开始寻找方法,这个过程中就涉及到了一个词语:“内网穿透”。

Mac

mac下面也有比较火的“内网穿透”的项目:ngrok。


概要原理

penetrate
远程控制(花生壳),FQ(ss),微信小程序开发(ngrok)都可以用这个图解释。

  1. 远程控制(花生壳):
    • 外部B:代表了自己在家的时候的机器(包含了路由了)。
    • proxy:代表花生壳自己的服务器,并且有对应的域名和端口。
    • 内部A:代表安装了花生壳客户端,想要被远程控制的公司内部机器。
      逻辑关系:
      外部B 请求 proxyproxy 找到与 内部A 之间的长链接,然后代理接受和发送RDP协议数据,达到远程控制的效果。
  2. FQ(ss):
    • 外部B:代表被访问的fb,tw等Q外内容。
    • proxy:代表ss服务商提供的服务器,提供了对应的地址和端口。
    • 内部A:代表安装了ss客户端的,想要FQ的机器。
      逻辑关系:
      ss使用了socket5的协议,ss客户端监控了特定的端口,内部A 上面的数据只要是通过特定的端口经过ss客户端,就会被特定算法对称加密,然后到达 proxy 会做对称解密,最后 proxy外部B 交互的到结果,最终再次加密返回给 内部A ,由 内部A 解密给给对应的应用。
  3. ngrok:
    • 外部B:微信小程序api或者银行链条方
    • proxy:公网的机器带有域名,ngrok的服务端
    • 内部A:自己的机器,ngrok的客户端
      逻辑关系:
      ngrok的客户端和服务端直接使用tcp开socket长链接通信。客户端和服务端都维护了映射表:服务端维护了二级域名和客户端之间的关系;客户端维护了二级域名加端口和自己所在局域网中的ip加端口之间的关系;外部B 先通过 proxy 找到对应的 内部B内部B 然后再调用对应的服务达到穿透的效果。

自己简易实现内网穿透

有现成的例子:
清晰的协议
java实现的客户端
java实现的服务端

认识java agent

java agent

agent

java agent是在JDK1.5之后引入的,是main方法之前的拦截器;其作用是在main之前之前执行agent中的逻辑。

假设 A 是主要业务逻辑应用所在进程; B 是要拦截在A的main方法之前的 agent。

那么agent有两种启动方式:

  • (1)B在A之前启动:通过 启动A时添加vm参数 -javaagent:xxxx.jar

    a. xxxx.jar包含了自己实现的agent类,实现规范中一种形式如下:

    public static void premain(String agentOps, Instrumentation inst){  
        //TODO 
    }
    

    b. xxxx.jar包含的资源配置文件Manifest中需要指明premain所在的类:

    Premain-Class: com.mypackage.MyAgent
    
  • (2)B在A之后启动:通过 VirtualMachine 的 attach 和 loadAgent 方法实现。

    VirtualMachine类所在包为:com.sun.tools.attach;相关代码:

    try{
        String pid = fetchPidId(); //获取到java 的 pid(各种方式)
        if( pid != null ){
            vm = VirtualMachine.attach( pid );    // 获取attach到的vm对象实例                                 
            vm.loadAgent(agentJarFilePath, args); // 加载agent
        }
    }finally {
        vm.detach();
    }
    

    a. agentJarFilePath包含了自己实现的agent类,实现规范中一种形式如下:

    public static void agentmain(String args, Instrumentation inst) {
        //TODO
    }
    

    b. agentJarFilePath包含了资源配置文件Manifest中需要指明agentmain所在的类:

    Agent-Class: war.Hatch
    

tips
资源描述文件Manifest里面agent相关的额外配置项如下:

  • Can-Redefine-Classes

    如果此值为true,可以修改已经加载的class的字节码。

  • Can-Retransform-Classes

    如果此值为true,表示支持重复修改class的字节码。这个和redefine的区别是,transform只是一个filter,没有修改本源字节码。

instrument

agent介绍中,我们可以看到instrument的身影,用来增强字节码。

instrument中可以:

1. 在 A 所在的JVM进程的BootstrapClassLoader 或者 SystemClassLoader(AppClassLoader)中加载额外的jar包。
2. 可以获得 A 中已经加载的所有的class类。
3. 可以添加 ClassFileTransformer 类来进行class文件(字节码)的修改。
    instrument.add

更多请参考API。这里有一篇比较好的理解,强烈推荐。

实践经验
  • 因为com.sun.tools.attach.VirtualMachine在tools.jar中,所以在(2)情况下要调用其attach和loadAgent的时候,需要在classpath中添加tools.jar。形如:

    java -classpath {JAVA_HOME}/lib/tools.jar:myTransport.jar Transport
    

    Transport 中的main方法包含了(2)中的示例代码。

  • 加入是用maven打包的推荐使用 maven-assembly-plugin,用其来管理资源管理文件配置项,示例配置如下:

    <manifestEntries>
        <Premain-Class>agent</Premain-Class>
        <Agent-Class>agent</Agent-Class>
        <Can-Redine-Classes>true</Can-Redine-Classes>
        <Can-Retransform-Classes>true</Can-Retransform-Classes>
    </manifestEntries>
    
  • 获取PID的示例命令:

    ps ax | grep 'java' | cut -d " " -f2
    
agent技术已知商业价值
  • 性能剖析:Btrace
  • 错误诊断:Btrace, Greys
  • AOP切面技术:Spring

JVMS7笔记

Structure of JVM


DataTypes:

  • primitive types

    • Integral Types : int, long, short, char。
    • Boolean Type : 计算的时候使用int。
    • Floating-Point Types : float, double。
    • the returnAddress Type : 开发不能直接使用。
  • reference types

    • 足够长来存储指针,长度一般与机器位数相同。

Runtime Data Areas:

  • The pc Register

    正常指向指令的地址。

    • 执行native方法的时候,pc是不存在值的。
  • Java Virtual Machine Stacks

    每个线程都有自己的stack,stack存储了frames。

    • frame

      A frame is used to store data and partial results, as well as to perform dynamic linking, return values for methods, and dispatch exceptions.

      A new frame is created each time a method is invoked. A frame is destroyed when its method invocation completes, whether that completion is normal or abrupt (it throws an uncaught exception). Frames are allocated from the Java virtual machine stack (§2.5.2) of the thread creating the frame. Each frame has its own array of local variables (§2.6.1), its own operand stack (§2.6.2), and a reference to the runtime constant pool (§2.5.5) of the class of the current method.

      • Local Variables

        一个local variable 占用一个word size;long,double占用两个word size,所以是两个local variable。 0 local variable 为被调用的method所在的object的reference(即this),方法的参数从1 local variable 开始连续 存储。

        JVM 设计的word size 不同于 word(8 bits) in computer science:
        
        The basic unit of size for data values in the Java virtual machine is the word--a fixed size chosen by the designer of each Java virtual machine implementation. The word size must be large enough to hold a value of type byte, short, int, char, float, returnAddress, or reference. Two words must be large enough to hold a value of type long or double. An implementation designer must therefore choose a word size that is at least 32 bits, but otherwise can pick whatever word size will yield the most efficient implementation. The word size is often chosen to be the size of a native pointer on the host platform.
        
      • Operand Stacks

        Each frame (§2.6) contains a last-in-first-out (LIFO) stack known as its operand stack.
        
      • Dynamic Linking

        The class file code for a method refers to methods to be invoked and variables to be accessed via symbolic references.
        
      • Special Methods

        每一个construction都是<init>方法;每一个默认的Void 且没有参数的contruction是<clinit>方法。
        they are never invoked directly from any Java virtual machine instruction, but are invoked only indirectly as part of the class initialization process.
        signature polymorphic(MethodHandler)这个也是JDK7加入的,为了实现JSR292,为在JVM上实现动态语言提供更多的支持。MethodHandle是JSR292的重要组成部分之一。invokevirtual 指令会对siglenature polymorphic methodMethodHandler以及子类)多特殊处理。
        
      • Exception

        同步异常(synchronous exception)抛出的方式:
        1. athrow 指令的调用
        2. JVM相关的Error,类loading,liking阶段相关的Exception,OutOfMemory,StackOverflow,RuntimeException的继承类等。
        异步异常(asynchronous exception)抛出的方式:
        1. Thread或者ThreadGroup的stop方法被触发。
        2. JVM内部的错误
            JVM允许异步的异常抛出存在一定的延迟,在最适合的时候记性处理--在这段延迟中,可以让JVM重排指令来优化代码和计算。(比较简单的实现方式是:在每条[control transfer instruction]执行的时候才去poll异步的异常。正是因为有这个保证,所以control transfer instruction之间的指令能够被JVM优化)
        

        在class文件中,excpetion handlers都存在table里面。

  • Heap

    Heap的管理是通过automacic storage management system,JVMS的实现可以实现自己heap管理机制。

  • Method Area

    It stores per-class structures such as the runtime constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization

    • 一般JVMS简单的实现是不会选择去处理Method Area的,但是这个版本的JVMS没有强制要求这么做。

    • Runtime Constant Pool

      A runtime constant pool is a per-class or per-interface runtime representation of the constant_pool table in a class file4.4). It contains several kinds of constants, ranging from numeric literals known at compile-time to method and field references that must be resolved at runtime. The runtime constant pool serves a function similar to that of a symbol table for a conventional programming language, although it contains a wider range of data than a typical symbol table.
      

Instruction Summary:

字节码指令流一般是按照字节排列的,只有lookupswitch & tableswitch 比较特别。

  • instructions with type info:

    指令集里面有部分是可以之间看出类型,比如iadd表示可以计算int类型的数据,fadd表示计算float类型的的数据等。但是要每个指令都支持所有的类型,是不现实的,因为一个byte只有8位。所以最后char,boolean,short等的add指令都会被转换成int的计算(使用iadd)。

  • instructions regardless of type info:

    指令集里面的类似push,pop,swap等是不带type信息的,只关心使用到的category of computational types。

    • Category 1:

      boolean, byte, char, short, int 的计算类型是 int; float计算类型位float;reference为reference;returnAddress为returnAddress;

    • Category 2:

      long的计算类型为long, double计算类型为double,

  • 指令概览

    • 支持的类型widening指令( i2l, i2f, i2d, l2f, l2d, and f2d):

      • int to long, float, or double
      • long to float or double
      • float to double
    • 支持的narrowing指令(i2b, i2c, i2s, l2i, f2i, f2l, d2i, d2l, and d2f):

      • int to byte, short, or char
      • long to int
      • float to int or long
      • double to int, long, or float

      类型转换过程中,会存在一定的信息丢失情况,具体需要具体翻看了解。

    • 支持的 control transfer instruction 分为以下几类:

      • conditional branch:

        ifeq, ifne, iflt, ifle, ifgt, ifge, ifnull, ifnonnull, if_icmpeq, if_icmpne, if_icmplt, if_icmple, if_icmpgt if_icmpge, if_acmpeq, if_acmpne

      • compound conditional brach:

        tableswitch, lookupswitch

      • unconditional brach:

        goto, goto_w, jsr, jsr_w, ret

    • 5种方法调用指令:

      • invokevirtual: 调用类实例的方法
      • invokeinterface: ???
      • invokespecial: init, private, super class的方法
      • invokestatic: 调用类的方法
      • invokedynamic: 关联到MethodHandle,看了相关的资料,暂时还没有领会其用途(???)
    • 同步, 分为两类:

      • method synchronization
      • block synchronization

        moniterenter,moniterexit 这两个指令配合完成同步。


Compiling for JVM

tableswitch是switch 连续的int 和 lookupswitch是switch离散的int。排序过的switch key 效率更高;tableswitch效率比lookupswitch高。

try-catch statements:

As a subtle point, note that the range of a catch clause is inclusive on the "from" end and exclusive on the "to" end.

A Java virtual machine implementation verifies that each class file satisfies the necessary constraints at linking time

String.intern() 会找到runtime constant pool中相同字符串的引用地址

skip Class file format & Verification of Class Files


Loading, Linking, and Initializing

VM Start-up

调用bootstrap classloader 加载 初始化类,然后加载main方法;一般jvm的实现都有 命令行参数 或者 提供 初始化参数的方式。

Creation and Loading

C代表一个interface or class;

N代表internal name of C in JVM implementation;

D触发了C的创建:D的runtime contant pool有C的引用 或者 D通过反射创建C

A class loader L may create C by defining it directly or by delegating to another class loader. If L creates C directly, we say that L defines C or, equivalently, that L is the defining loader of C.

When one class loader delegates to another class loader, the loader that initiates the loading is not necessarily the same loader that completes the loading and defines the class. If L creates C, either by defining it directly or by delegation, we say that L initiates loading of C or, equivalently, that L is an initiating loader of C.

The Java virtual machine uses one of three procedures to create class or interface C denoted by N:

  • If N denotes a nonarray class or an interface, one of the two following methods is used to load and thereby create C:

    • If D was defined by the bootstrap class loader, then the bootstrap class loader initiates loading of C.

    • If D was defined by a user-defined class loader, then that same user-defined class loader initiates loading of C.

  • Otherwise N denotes anarray class.An array class is created directly by the Java virtual machine, not by a class loader. However, the defining class loader of D is used in the process of creating array class C.

在loading的阶段出现异常,抛出LinkageError的继承类。

< $$$ N, L_d $$$ > : N denotes the name of the class or interface and $$$ L_d $$$ denotes the defining loader of the class or interface.

$$$ N^{L_i} $$$ : N denotes the name of the class or interface and $$$ L_i $$$ denotes an initiating loader of the class or interface.


Linking:

如果JVM在Verification or Resolution class C的时候抛出了ClassNotFoundException的时候,会被包装成NoClassDefFoundError.(Loading of superclass 是 Rsolution逻辑的一部分)

  • Verification
  • Prepare
  • Resolusion: 这个可能在真正使用的时候(懒加载,invokedynamic指令)才会使用到;也有情况是直接使用。

Initializing:

ClassLoader.loadClass() 与 Class.forName()
ClassLoader.loadClass(String name, boolean resolve),其中resolve默认为false,即只执行类装载的第一个阶段。

Class.forName(String name, boolean initialize, ClassLoader loader), 其中initialize默认为true,即执行到类装载的第三个阶段。

理解CPU steal time

Netflix 很关注CPU的Steal Time。他们的策略是:如果是当前虚拟机的Steal Time 超过了你们设置的 阈值,他们会关闭这台虚拟机并且在另外一台物理机上面重启。

如果你想要部署虚拟环境(例如:Amazon EC2), steal time就是你想要关注的性能指标之一。 如果这个指标的数值很高,那么说明机器状态非常糟糕。什么是steal time?什么会引发高steal time?多少才是警戒值(你需要做什么)?

CPU Steal Time 的定义

From ibm:

Steal time is the percentage of time a virtual CPU waits for a real CPU while the hypervisor is servicing another virtual processor.

你的虚拟机(VM)会与虚拟环境的宿主机上的多个虚拟机实例共享物理资源。其中之一共享的就是CPU时间切片。如果你的VM的物理机虚拟比是1/4, 那么它的CPU使用率不会限制于25%的CPU时间切片-它能够使用超过它设置的虚拟比。(有别于内存的使用,内存大小是严格控制的)。

哪里可以看到CPU Steal Time?

你可以使用Linux 的 TOP 命令来看到实时的一些性能指标。CPU相关的其中一行内容如下:
top
两个你可能较为熟悉的是 %id(空闲 百分比) 和 %wa(I/O 等待 百分比)。 如果 %id 很低, 那么说明CPU的工作负载很大并且没有多少计算负载能力剩余。 如果 %wa 很高,则说明瓶 CPU 处于等待计算的状态,但是正在等待I/O活动的完成(类似 从数据库中获取存储在 磁盘上 的一行数据)。

%st(percent steal time) 是CPU展示的最后一个性能指标。

CPU Steal Time - 类比售票厅

假设你打算买了若干张最新的好莱坞大片的电影票,且有两条队伍等待买票和一个售票口:

Movie Theater

如果我们把 CPU steal time 性能指标 类比成 售票的过程, 那么过程就是如下:

  • 0% Steal Time - 现在是礼拜三下午场:售票口正在工作,先处理第一条队伍的电影观众,然后处理第二条,然后第一条,然后第二条,轮流进行。处理的很快,且没有人在等待。

  • 50% Steal Time - 现在是礼拜五晚上: 在队伍中的一个人有一半的时间需要等待另一个在售票口的人完成卖票,而不能立刻买到票。卖票的时间更长了。

  • 100% Steal Time - 现在是礼拜五晚上并且 现金出纳金 坏了:所有人都在等待。

为什么高 Steal Time 会对web应用有更大的影响

如果有你在负载未满的物理机器上面运行一个长时间的计算任务,那么它可能会使用超过它额定的CPU切片 时间。过一段时间,可能其他的VMs可能也会需要超过它们额定量的CPU切片 时间,所以这个任务的执行会变慢。对于长时间计算任务而言之,这个情况可能并不是不能接受的:它可能是会晚点一完成或者也可能更快的完成(由于它能够使用更多的资源)。

然后,这种情况能够时代web应用停止响应。对于实时任务,类似快速响应许多的web请求,性能下降到1/4会对请求队列执行对应备选逻辑—中断请求。

Steal Time远高于0的原因

这里有两种可能性:

  1. 你需要一个额定更多CPU资源的虚拟机(你的虚拟机问题)

  2. 物理机已经超卖了并且多个虚拟机之间在激烈的竞争资源(你的虚拟机不是问题)

提示:你不能通过看当前被影响的虚拟机实例的CPU性能指标来判断你所遇到的场景。(1 or 2) 当你有很多的虚拟宿主机上分别都部署了相同职责的服务程序(可能作为集群)时,就比较容易知道自己遇到的问题了。

资源图片

  • 是否 %st(CPU Steal Time Percentage) 在所有机器上面都上涨了?

    这个意味着你的虚拟机在使用更多的CPU资源。你需要为你的虚拟起增加更多的CPU资源的配额。

  • 是否%st(CPU Steal Time Percentage) 只在一部分机器上面陡峭增长?

    这个意味着物理机器被超卖了。把你自己的虚拟机挪到另一个物理机器去吧。

所有,什么时候你应该担心?

一般的参考标准-如果steal time 超过了10%并且持续了20分钟,那么虚拟机就可能性能下降了

当这种情况发生:

  1. 关闭虚拟机并且挪到另一台物理机器上面

  2. 如果steal time维持在很高的数值, 那么增加CPU资源配额。

  3. 如果steal time维持在很高的数值, 联系你的虚拟机提供商。你的虚拟机提供商有可能在超卖物理机。

翻译自 这里

公司的应用最近都已经迁移到自家的云计算平台上面了,所以也开始出现了无故Load飙升的情况,最后定位出来原因时虚拟机CPU资源竞争的情况。新知识Get!

亲身经历icloud账号被黑事件

2015-10-05

下午3点:当时在高速上面聊天,老婆手机被无故重启,重启以后屏幕内容如下:
XXX,如需解锁请在24小时内联系QQ:2075633961

下午4点半:下高速直奔杭州平海路的apple销售点,找天才吧的技术人员解决,得到答复:他们无法解决,请打4006272273 解决。打了近1个小时的电话,由于换电话号码,忘记提示问题,一直没有成功。

下午6点半:驱车回家,在家里面打开电脑,这个时候完了,电脑因为使用了同一个appId,也被锁了。老婆明天还要使用电脑办公,如何解决?打电话给4006272273,通过耐心尝试提示问题,终于想起来了2个,解决了icloud的密码问题,icloud账号重新找回了并且修改了密码。手机能够重新激活了,但是数据被抹掉了,算了,大不了,花半个小时重装软件。问题是现在电脑如何解决?还是需要4位的pin码啊。继续4006272273转到mac部门沟通,说是要到实体店售后解决。接着我就电话给实体店售后,售后告诉我不能解决,要4006272273有更高的权限解决。

400热线的客服的让我找实体店技术人员解决,实体店技术人员让我找400热线的客服解决? 所以这个就是个死循环了? 这个是什么情况???

带着不解和火气直奔了苹果的实体店,很理性的询问了技术人员,咨询情况,经过近15分钟的沟通,然后得到的答复是:先要预约,然后要购买凭证,最后电脑的数据会被抹掉。总结下就是:

处理周期是7-15天,然后数据会被抹掉,不能恢复!

我心里就明白了:苹果天才吧的技术人员 是 根本不能解决这个问题,然后才和400热线的客服人员进行推脱的!苹果的售后竟然存在如此大的问题,最让人受不了的方式就是这种推诿,浪费消费者的时间和精力。如果早点告诉我们:我们解决不了。我们就会找另外的方案解决了。

最后的结果就是,理性的接受勒索,和对应的QQ ID沟通,支付宝200,然后得到pin码,电脑解锁,保全数据,重装系统,对所有相关邮箱账号都重置了密码和绑定了手机,开启二次验证。icloud账号也开启了二次验证,3天后生效。

虽然icloud账号开启了二次验证,也只是防止被修改密码,如果icloud账号泄露,那么电脑一样可以被锁和抹掉数据!!!

总结有两种解决办公电脑不受icloud账号影响的方式:

  1. 最安全方案:关闭mac上的找回我的mac功能。代价是人肉保证电脑不被偷。
  2. 次优方案:软件都使用正版。这种方式最大程度上面减少电脑方面泄露icloud信息,但是不能保证解决icloud相关邮箱和手机icloud信息泄露的情况。

小计这件事情的目的:

  1. 是提醒身边使用mac的朋友,当心icloud账号被人盯上了。
  2. 原来apple的售后也是有这么无力的时候,但是却又极力的不想表现出来,让人恶心,基本的诚实也没有。
  3. 之前都是听安全部的同事分享黑客和黑产内容,现在算是亲身经历了。2075633961 这个QQ号和他聊天的感觉,看起来是一个专业的客服人员,举报或许有点用吧,现在po一下这个QQ。

本地缓存组件选型

需求: 记录系统执行数据,能持久化,容错。

Guava

官网就没有说有persistent的功能,直接略过。

ehcache

企业版需要缴费,然后持久化功能和保证重启可用的功能都需要缴费。

看看开源版ehcache的是否可以满足,现状是:可以在只是DiskStore相关配置,让cache把内容序列化后直接overflow到文件。
现在问题是:

重启

重启以后,是重新新建文件并覆盖同名文件—也就是之前同名cache生成的文件。尝试出使用自己实现的RebootCacheLoaderFactory implemets BootstrapCacheLoaderFactory可以来创建 RebootCacheLoader implements BootstrapCacheLoader来实现内容。

RootbootCacheLoaderFactory:

public class RebootCacheLoaderFactory extends BootstrapCacheLoaderFactory {
    @Override
    public BootstrapCacheLoader createBootstrapCacheLoader(Properties properties) {
        return new RebootCacheLoader();
    }
}

RebootCacheLoader:

public class RebootCacheLoader implements BootstrapCacheLoader {

    @Override
    public void load(Ehcache cache) throws CacheException {
        cache.put(new Element("haha"+Math.random(), Math.random()));
    }

    @Override
    public boolean isAsynchronous() {
        return true;
    }

    public Object clone(){
        return new RebootCacheLoader();
    }
}

Main:

String path = "/tmp/ehcache/";
String cacheManagerName = "cacheManagerName";
Long diskSizeBytes = 4 * 1024 * 1024 * 1024L;
String cache1Name = "40006_1232131231312";
String cache2Name = "40002_1232131231312";

Configuration configuration = new Configuration();
configuration.setName(cacheManagerName);
CacheConfiguration defaultCacheConfiguration = new CacheConfiguration();
defaultCacheConfiguration.overflowToOffHeap(false);
defaultCacheConfiguration.overflowToDisk(true);
defaultCacheConfiguration.maxEntriesLocalHeap(1); //0 is not limited, DiskStore, MemStore will all have the element; 1 means just the last element will be in memory.
defaultCacheConfiguration.maxBytesLocalDisk(2, MemoryUnit.MEGABYTES); //模糊的界定,异步写入,主线程结束了,还有一些记录没有记录的,会丢失数据。
defaultCacheConfiguration.addBootstrapCacheLoaderFactory(new CacheConfiguration.BootstrapCacheLoaderFactoryConfiguration().className(
            "com.daniel.test.RebootCacheLoaderFactory"
    ));

configuration.setDefaultCacheConfiguration(defaultCacheConfiguration);

CacheManager cacheManager = new CacheManager(configuration);

cacheManager.addCache(cache1Name);

cacheManager.addCache(cache1Name)方法里面clone一个defaultCache,然后改个名字,然后initialize cache完成之后,调用cache.bootstrap()方法:

public void bootstrap() {
    if (!disabled && bootstrapCacheLoader != null) {
        bootstrapCacheLoader.load(this);
    }

}

然后就可以load到自定义信息了,因为底层就是用了java自带序列化来存储Element对象,方法在:DiskStorageFactory.serializeElement()

持久化

从最近本的cache.put()方法不断debug进去,发现DiskStore.put()引用了segment.put(),然后Element的行记录的包装类PlaceHolder里面的install方法调用了如下内容:

public void installed() {
    DiskStorageFactory.this.schedule(new PersistentDiskWriteTask(this));
}

看着感觉很悲剧,果然看到里面是一个ScheduledThreadPoolExecutor在执行,实体是DiskStorageFactory里面的diskWriter,然后是默认生成的,还不能修改队列啥的:

diskWriter = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, file.getName());
        t.setDaemon(false);
        return t;
    }
});

这个构造函数,消息队列是默认11个的PriorityQueue,所以机器中断可能导致数据还在queue里面没被写入到磁盘。这个组件不能用了!!!

OSCache

名声挺好的,搜了下,已经不维护了,略过

JCS

JCS是插件式管看着很棒,persistent功能介绍也很强大,符合我的需求,看了一眼bugfix清单,关于persistet的bug那么多,最新的版本还是beta的,完全不敢用,不想自找麻烦。

FQueue

转换下思路了。如果说持久化要做到可靠性最佳,理论上面就应该是同步插入数据到磁盘;如果还要保证消费的顺序性,想了一种方案,如下:

思路

虽然很开心的想出来了,但是可靠性却不能保证啊。我们的存储使用的是HBASE,scheme设计是:rowkey用index,所有相同index的记录都需要记录到一行记录里面。如果我再搞一个持久化的重试队列,Queue的Element先poll出来,push到retryQueue extends LinkedList,并在内存存储retryQueue的Element和位置的关系Map,然后等异步插入HBase数据完成之后,利用内存关系来做删除,这个时候有两个问题:

  1. fQueue使用是FIFO的Queue,如何在记录位置并删除呢,效率是否有影响?想象下多个线程同时操作一个文件。如果要做, 可能需要学ehcache的方式,在内存里面映射文件为一个map,做segment分段加锁,然后在内存中删除,同时交给一个只有一个线程的线程池异步删除对应文件数据,那这样最多就2个线程抢占一个文件,一个写入,一个删除;那么问题来了,因为是异步,所以可能存在crash的时候内存删除了,文件数据没删除的情况,对异常恢复造成影响,类似之后提到的2问题。

  2. 插入HBase的时候,机器crash了,我们不知道是否真的插入成功了,就需要在机器重启的时候重新插入,这个时候会在同一行记录里面append内容一样的column了。

换一种思路:
我每一个index下的内容都生成一个持久化文件,并在内存中存储一个mark标记,标记是否index文件生成完了,监听器监听 生成完的状态,然后交给一个线程处理这个index相关的一个文件,因为内容是顺序的,所以直接拼装成一条记录,并且写入,写入成功之后,删除这个index对应的文件。异常状况处理如下:

  1. index下内容生成一半,crash,重启以后首先消费掉这些数据并做插入到HBase,保证异常数据也能够被系统得到。

  2. 插入HBase时,crash,重启以后同样重复消费文件数据,构造一条记录,插入。如果上次已经插入通用的rowkey数据,那么HBase自己会做update,那么业务上面完全没有影响,就是DB方面多update了一次。

恩,现在感觉完美了。

FQueue默认文件大小是300M,用400个线程同时创建文件,ssd 硬盘,2.5GCPU的配置直接卡爆。然后调整为默认为1M。重写了三个类:FQueue.java, FSQueue.java, LogEntity.java

HMFQueue.java

public HMFQueue(String path) throws Exception {
    fsQueue = new HMFSQueue(path, 1024 * 1024 * 1); //默认是1M大小
}

@Override
//not thread safe, be careful , 添加了iterator的支持,需要修改FSQueue和LogEntity
public Iterator<byte[]> iterator() {
    try {
        fsQueue.curseToHead();
    } catch (IOException e) {
        log.error(" cannot open the file!",e );
        throw new UnsupportedOperationException("failed to move to head!");
    } catch (FileFormatException e) {
        log.error("failed to move to head! the file format is wrong",e );
        throw new UnsupportedOperationException("failed to move to head!");
    }

    return new Iterator<byte[]>() {

        @Override
         public boolean hasNext() {
            return fsQueue.hasNext();
        }

        @Override
        public byte[] next() {
            return fsQueue.readNextAndMove();
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException("remove operation is not supported here!");
        }
    };
}

HMFSQueue.java 添加如下方法支持文件的重头读取和遍历

//not thread safe 定位到文件的头部,更新index
public void curseToHead()  throws IOException, FileFormatException {
    int fileNum = -1;
    File dir = new File(path);
    if(dir.exists() && dir.isDirectory()){
        if(dir.listFiles() != null && dir.listFiles().length > 0){
            for(int i = 1; i < Integer.MAX_VALUE; i++){
                String fileName = path + fileSeparator + filePrefix + "data_" + i + ".idb";
                File file = new File(fileName);
                if(file.exists()){
                    fileNum = i;
                    break;
                }
            }
            if(fileNum != -1){
                if (writerHandle.getCurrentFileNumber() == fileNum) {
                    readerHandle = writerHandle;
                }else{
                    readerHandle = createLogEntity(path + fileSeparator + filePrefix + "data_" + fileNum + ".idb", db,
                            fileNum);
                }
                readerHandle.resetReaderPosition();
            }
        }
    }
}

//not thread safe 判断是否有下一条数据
public boolean hasNext(){
    byte[] b = null;
    try {
        b = readerHandle.readNext();
    } catch (FileEOFException e) {
        int nextfile = readerHandle.getNextFile();
        readerHandle.close();
        // 更新下一次读取的位置和索引
        db.putReaderPosition(HMLogEntity.messageStartPosition);
        db.putReaderIndex(nextfile);
        if (writerHandle.getCurrentFileNumber() == nextfile) {
            readerHandle = writerHandle;
        } else {
            try {
                readerHandle = createLogEntity(path + fileSeparator + filePrefix + "data_" + nextfile + ".idb", db,
                        nextfile);
            } catch (IOException e1) {
                log.error("failed to read file: " + path + fileSeparator + filePrefix + "data_" + nextfile + ".idb",e1);
            } catch (FileFormatException e1) {
                log.error("the file has wrong format: " + path + fileSeparator + filePrefix + "data_" + nextfile + ".idb", e1);
            }
        }
        try {
            b = readerHandle.readNext();
        } catch (FileEOFException e1) {
            log.error("read new log file FileEOFException error occurred",e1);
        }
    }
    if (b != null) {
        return true;
    }
    return false;
}

//not thread safe 读取下一个并且更新索引
public byte[] readNextAndMove(){
    byte[] b = null;
    try {
        b = readerHandle.readNextAndMove();
    } catch (FileEOFException e) {
        int nextfile = readerHandle.getNextFile();
        readerHandle.close();
        // 更新下一次读取的位置和索引
        db.putReaderPosition(HMLogEntity.messageStartPosition);
        db.putReaderIndex(nextfile);
        if (writerHandle.getCurrentFileNumber() == nextfile) {
            readerHandle = writerHandle;
        } else {
            try {
                readerHandle = createLogEntity(path + fileSeparator + filePrefix + "data_" + nextfile + ".idb", db,
                        nextfile);
            } catch (IOException e1) {
                log.error("failed to read file: " + path + fileSeparator + filePrefix + "data_" + nextfile + ".idb", e1);
            } catch (FileFormatException e1) {
                log.error("the file has wrong format: " + path + fileSeparator + filePrefix + "data_" + nextfile + ".idb", e1);
            }
        }
        try {
            b = readerHandle.readNextAndMove();
        } catch (FileEOFException e1) {
            log.error("read new log file FileEOFException error occurred",e1);
        }
    }
    return b;
}

//删除消息队列相关数据文件
public void delQueue() throws IOException, FileFormatException {
    FileUtils.deleteDirectory(new File(path));
}

HMLogEntity.java

public HMLogEntity(String path, LogIndex db, int fileNumber,
                 int fileLimitLength) throws IOException, FileFormatException {
    ........
    if (file.exists() == false) {
        createLogEntity();
        //FileRunner.addCreateFile(Integer.toString(fileNumber + 1)); //注释掉,如果文件切片为1M时候,提前创建有问题:由于是异步的,消费的时候会存在还没有创建成功,写入失败的情况。
    } else {
        ........
    }      
}

测试下性能,单线程写20W的数据花了9s;

但是多线程写入多个文件性能和稳定性可能有问题了:

  1. 400个线程,每个线程创建自己的文件,写入300条数据,遍历300条数据,删除自己创建写入的文件总共要6.1s,且一切正常。

  2. 400个线程,每个线程创建自己的文件,写入400条数据,遍历400条数据,删除自己创建写入的文件出现写入失败和读取失败的情况:240个线程写入完成,133个线程遍历完成,且没有异常抛出。

  3. 600个线程,每个线程创建自己的文件,写入300条数据,遍历300条数据,删除自己创建写入的文件出现写入失败和读取失败的情况:244个线程写入完成,160个线程遍历完成,且没有异常抛出。

1的情况已经是达到了临界值。2,3都会出现不稳定的情况,我去,这个是啥原因呢??百思不得其解。

Dynamic Bean

突然对一些基础类库的代码感了兴趣,于是最先看起了Apache的commons-beanutils这个包,然后顺藤摸瓜式的了解JavaBeans和Dynamic Bean相关的知识。

common-beanutils-1.9.2

此包主要功能如下:

创建动态的JavaBean及相关的util类型代码。

主要类的关系图如下:

主要类图

  • DynaClass, DynaProperty, DynaBean用来动态生成JavaBean, 主要使用了Java的原生包:java.lang.reflect & java.beans

  • BeanUtilsBean是单例, 包含拷贝属性,克隆bean等util方法。

举个栗子

import org.apache.commons.beanutils.BasicDynaClass;
import org.apache.commons.beanutils.DynaBean;
import org.apache.commons.beanutils.DynaClass;
import org.apache.commons.beanutils.DynaProperty;

/**
 *
  * @author www.javadb.com
  */
public class DynaBeanExample {

    private final String NR_OF_WHEELS = "numberOfWheels";

    private void runExample() {

        DynaClass dynaClass = new BasicDynaClass("Car", null,
            new DynaProperty[]{
                new DynaProperty(NR_OF_WHEELS, Integer.TYPE)});

        try {
            DynaBean car = dynaClass.newInstance();
            car.set(NR_OF_WHEELS, 4);

               System.out.println("Number of wheels: " + car.get(NR_OF_WHEELS));

            System.out.println("DynaBean is instance of DynaClass: " + car.getDynaClass().getName());

        } catch (IllegalAccessException | InstantiationException ex) {
            System.err.println(ex.getMessage());
        }
    }

   /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        DynaBeanExample ac = new DynaBeanExample();
        ac.runExample();
    }
}

所以看着这个使用的方式感觉还是蛮有意思的,可以动态的生成自己想要的“Java Bean”

这个时候就开始好奇这个包会有什么问题呢?比如性能上面?然后就搜到了这个Drools很早就开始反对使用这种方式来构建Java Bean,理由很简单,系统兼容性不好,说这个包虽然实现的Dynamic Bean的功能,但是生成的DynaBean是不符合JavaBeans的规范的,将会引发一系列的代码兼容性的问题,所以不建议商业系统环境使用这个包—例如主流的ORM框架都只支持规范的JavaBeans。我们看代码可以发现DynaBean使用的是一个Map<String, DynaProperty>来存储属性的。

那么现在问题来了,什么是JavaBeans ?

JavaBean

参阅JavaBeans(TM) Specification官方的文档,移步解读大纲。所以这个包是通过使用Introspection相关的类来实现Dynamic Bean,而不是构造符合JavaBeans规范的类.所以Drools就推荐使用了cglib来实现Dynamic Bean, 移步此参考

JavaBeans(TM)

JavaBeans是为了定义出最小的可复用的组件,具备移植性和复用性,类似com+组件。

Java Bean由properties,event,method(listen methods)组成。

JavaBeans定义了一堆规范和利用了listener design pattern来完成properties变化事件和自定义事件的监听。

JavaBeans交互图