乐趣区

关于java:JAVA动态字节码实现方式对比之Byte-Buddy

1、背景

  JAVA 为什么要动静操作字节码?除 Byte Buddy 说的因为 JAVA 有严格的类型校验,在开发接口交互的过程中限度了类型编译,其实还有其余很多方面的利用。比如说通用形象,将脚本形容动静生成 JAVA 代码,这种流程泛华动静生成代码的利用场景非常广泛。
  在 JAVA 字节码操作有几种惯例形式,在技术选型时,咱们次要思考如下几点:

  • 适用范围
  • 受欢迎水平
  • 开发难度
  • 保护老本

2、加强选型

  • java.lang.reflect.Proxy:https://docs.oracle.com/javas…
  • javassist: https://github.com/jboss-java…
  • byte buddy:https://bytebuddy.net/#/tutorial
  • cglib:https://github.com/cglib/cglib
  • java agent:https://docs.oracle.com/javas…
  • groovy://TODO

3、Byte Buddy

3.1 三种加强形式

  • subclass(创立)
      通过继承已有的类,动态创建一个新类。
      subclass 能够自定义属性、办法,也能够重写现有办法。
      subclass 的一个益处是,类是新建,运行时加载不存在类抵触的问题;毛病是,对已加载的类不能加强,因为编译时没有任何类会依赖新增类。

    /**
     * 创立一个空类 
     * 
     */@Runner
    public class SimpleCreateRunner implements Runnable {
     static String newClassName = "net.bytepuppy.subclass.HelloWorld";
     @SneakyThrows
     @Override public void run() {
     // DynamicType.Unloaded,顾名思义,创立了字节码,但未加载到虚拟机
     DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
     // 继承 Object.class
     .subclass(Object.class)
     // 指定固定的名字
     .name(newClassName)
     // 创立字节码
     .make();
     // 将字节码保留到指定文件
     dynamicType.saveIn(Consts.newFile(Consts.CLASS_OUTPUT_BASE_DIR));
     System.out.println("save class:" + Consts.CLASS_OUTPUT_BASE_DIR + newClassName);
     }
    }
  • redefine(重写)
      重写顾明思议就是能够对一个现有类的属性、办法进行增、删、改。
      重写的前提是 redefine 后的类名不变,如果重命名 redefine 后的类,其实跟 subclass 成果相当。
      属性、办法被 redefine 后,原定义(属性、办法)会失落,如同类被重写了一样,这也是我将 redefine 翻译成重写的起因。

  JVM runtime redefine 一个类,不能被加载到 JVM 中,因为会报错:java.lang.IllegalStateException: Class already loaded: class xxx

  JVM runtime 类替换的的办法之一,是 JVM 热加载。byte buddy 通过 ByteBuddyAgent.install() + ClassReloadingStrategy.fromInstalledAgent()封装了简洁的热加载调用。
  但遗憾的是,JVM 热加载不容许增减原 class 的 schema(比方增减属性、办法),因而应用场景十分受限。批改 Schema 后热加载报错:UnsupportedOperationException: class redefinition failed: attempted to change the schema (add/remove fields)

  • rebase(加强)
      rebase 性能与 redefine 相当,也能够已有类的办法、属性自定义增删改。
      rebase 与 redefine 的区别,redefine 后的原属性、原办法失落;rebase 后的原属性、原办法被拷贝 + 重命名保留在 class 内。
      rebase 能够实现一些相似 java.lang.reflect.Proxy 的代理性能。但 rebase 与 redefine 一样,热加载类的问题仍然存在。见:https://github.com/raphw/byte-buddy/issues/104

3.2、加载创立类

  类加载器参考:https://blog.csdn.net/briblue/article/details/54973413。自定义类加载器,个别重写 findClass 即可,loadClass 不重写。
  byte buddy 加强后创立的类,如果类名是新的,都能够通过 ClassLoader 加载。
  鉴于 ClassLoader 的双亲委派模式:AppClassLoader -> ExtClassLoader -> BootstrapClassLoader,新创建的类能够间接应用 AppClassLoader 来加载,新类在整个 JVM 中都是可见的。

  byte buddy 封装了几个罕用的 ClassLoader 相干调用:

  • ClassLoadingStrategy.BOOTSTRAP_LOADER: 在 Byte Buddy 中代表 BootstrapClassLoader,但赋值为 Null,不能间接应用。ClassLoadingStrategy.BOOTSTRAP_LOADER 的作用是用于构建 ByteArrayClassLoader。(BootstrapClassLoader 是用 C ++ 编写,Java 中没有间接类能够援用)
  • ByteArrayClassLoader:byte buddy 自定义类加载器,继承自 ClassLoader,未重写 loadClass 办法,合乎双亲委派模式。即用 ByteArrayClassLoader 加载的类,在 JVM 中全局可见。ChildFirst
  • ByteArrayClassLoader.ChildFirst: ChildFirst 继承了 ByteArrayClassLoader,然而重写了 loadClass 办法,毁坏了双亲委派模式。

  ClassLoader 与 ByteArrayClassLoader.ChildFirst 代码区别比照如下:

  • ClassLoader:

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (SYNCHRONIZATION_STRATEGY.initialize().getClassLoadingLock(this, name)) {
    // 在本地 loader 已加载内容中查找类
            Class<?> type = findLoadedClass(name);
            if (type != null) {return type;}
    // 找不到,跳过父类尝试,间接从本地 loader 的 findClass 中找
    // 这里有一个问题:如果用这个 ChildFirstClassLoader 加载的类,不会尝试父类查找。换句话讲,同名类能够被父 loader 和子 loader 同时加载,但 class 却不相等
            try {type = findClass(name);
                if (resolve) {resolveClass(type);
                }
                return type;
            } catch (ClassNotFoundException exception) {
                // If an unknown class is loaded, this implementation causes the findClass method of this instance
                // to be triggered twice. This is however of minor importance because this would result in a
                // ClassNotFoundException what does not alter the outcome.
    // 本地 loader 找不到,在尝试从父 loader 中找
                return super.loadClass(name, resolve);
            }
        }
    }
  • ByteArrayClassLoader.ChildFirst

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (SYNCHRONIZATION_STRATEGY.initialize().getClassLoadingLock(this, name)) {
    // 在本地 loader 已加载内容中查找类
            Class<?> type = findLoadedClass(name);
            if (type != null) {return type;}
    // 找不到,跳过父类尝试,间接从本地 loader 的 findClass 中找
    // 这里有一个问题:如果用这个 ChildFirstClassLoader 加载的类,不会尝试父类查找。换句话讲,同名类能够被父 loader 和子 loader 同时加载,但 class 却不相等
            try {type = findClass(name);
                if (resolve) {resolveClass(type);
                }
                return type;
            } catch (ClassNotFoundException exception) {
                // If an unknown class is loaded, this implementation causes the findClass method of this instance
                // to be triggered twice. This is however of minor importance because this would result in a
                // ClassNotFoundException what does not alter the outcome.
    // 本地 loader 找不到,在尝试从父 loader 中找
                return super.loadClass(name, resolve);
            }
        }
    }

      联合 Byte Buddy 应用状况:

  • ClassLoadingStrategy.Default.WRAPPER:构建一个 ByteArrayClassLoader,合乎双亲委派规定。
  • ClassLoadingStrategy.Default.CHILD_FIRST:构建一个 ByteArrayClassLoader.ChildFirst,rebase、redefine 的类,能够被加载,但在 AppClassLoader 中不意识同名类。

3.3、替换加载类

  byte buddy 加强后创立的类,如果想类名不变,就不能够通过 ClassLoader 加载了,因为 JVM 回绝反复加载雷同类:java.lang.IllegalStateException: Class already loaded。
  替换已加载的类有两种形式:

  • 热加载
    参考:https://developer.aliyun.com/article/65023

  热加载须要借助 java agent 的 Instrumentation.redefineClasses。中央
byte buddy 提供了便捷的热加载实现:ByteBuddyAgent.install() 配合 ClassReloadingStrategy.fromInstalledAgent()。
  热加载有极大的应用限度:不容许批改已有的属性、办法,如果 class schema 变动,JVM 回绝重载批改类。这意味着咱们很难用热加载的形式编写切面逻辑。

  • 懒加载
      JVM 有一个个性,启动后,类要到应用的时候才加载。 这意味着,如果咱们不间接援用类,触发类加载,那么在此之前咱们都能够自在替换加强类。
      加强类的生成产生在构建时,因为构建是在另一个 JVM 中实现的,所以不影响运行时类加载。
    为了实现懒加载,byte buddy 构建了几个有用的类:
  • TypeDescription
      类型形容对象。用此对象包装的类,不会触发类加载,但能够取得包装类的各种信息。
  • TypePool
      TypeDescription 类型池。用 TypePool.Default.ofSystemLoader()语句能够取得但前 ClassLoader 下所有的类形容,但不会触发类加载。
      typePool.describe(“{your_class_string_name}”).resolve()能够取得对应类的 TypeDescription。

3.4、java agent

  参考:https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html

  在我的项目规模宏大的时候,下面替换加载类的形式都限度太多,不实用。

  java agent 能够在类加载前,批改(transform)类,防止热加载,同时还能对业务逻辑进行加强。

  替换加强类,次要是通过 java.lang.instrument.Instrumentation,Java Agent 之所以能有用,次要还是 Java Agent 的两个入口提供了 java.lang.instrument.Instrumentation 的拜访入口:

  • premain
      JVM 初始化后被调用,办法为:public static void premain(String arguments, Instrumentation instrumentation) {…}
      -javaagent:jarpath[=options]形式增加 JVM 启动参数,permain 被调用,agentmain 即使实现也不会调用。
  • agentmain
      JVM 启动后被调用,办法为:public static void agentmain(String arguments, Instrumentation instrumentation) {…}

  agentmain 被调用有三个条件:
  1)agent jar 的 manifest 必须显式定义属性 Agent-Class;
  2)Agent-Class 指定类,必须定义 agentmain 办法;
  3)agent jar 在 JVM 启动的 classpath 门路中。

  例如:byte buddy 定义了 net.bytebuddy.agent.Installer 就是用 agentmain 的形式获取 Instrumentation 类。咱们解压 byte-buddy-agent-1.10.23-SNAPSHOT.jar,cat META-INF/MANIFEST.MF,能够找到 Agent-Class 定义:

../META-INF$ cat MANIFEST.MF 
Manifest-Version: 1.0
Bundle-Description: The Byte Buddy agent offers convenience for attach
 ing an agent to the local or a remote VM.
Bundle-License: http://www.apache.org/licenses/LICENSE-2.0.txt
Bundle-SymbolicName: net.bytebuddy.byte-buddy-agent
Built-By: liuh
**Agent-Class: net.bytebuddy.agent.Installer**
Bnd-LastModified: 1616574091622
Bundle-ManifestVersion: 2
Can-Redefine-Classes: true
Import-Package: com.sun.tools.attach;resolution:=optional,com.ibm.tool
 s.attach;resolution:=optional
Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.5))"
Can-Set-Native-Method-Prefix: true
Tool: Bnd-3.5.0.201709291849
Export-Package: net.bytebuddy.agent;version="1.10.23"
Premain-Class: net.bytebuddy.agent.Installer
Bundle-Name: Byte Buddy agent
Bundle-Version: 1.10.23.SNAPSHOT
Multi-Release: true
Can-Retransform-Classes: true
Created-By: Apache Maven Bundle Plugin
Build-Jdk: 1.8.0_144
退出移动版