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的一个益处是,类是新建,运行时加载不存在类抵触的问题;毛病是,对已加载的类不能加强,因为编译时没有任何类会依赖新增类。

    /** * 创立一个空类  *  */@Runnerpublic 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.0Bundle-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.txtBundle-SymbolicName: net.bytebuddy.byte-buddy-agentBuilt-By: liuh**Agent-Class: net.bytebuddy.agent.Installer**Bnd-LastModified: 1616574091622Bundle-ManifestVersion: 2Can-Redefine-Classes: trueImport-Package: com.sun.tools.attach;resolution:=optional,com.ibm.tool s.attach;resolution:=optionalRequire-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.5))"Can-Set-Native-Method-Prefix: trueTool: Bnd-3.5.0.201709291849Export-Package: net.bytebuddy.agent;version="1.10.23"Premain-Class: net.bytebuddy.agent.InstallerBundle-Name: Byte Buddy agentBundle-Version: 1.10.23.SNAPSHOTMulti-Release: trueCan-Retransform-Classes: trueCreated-By: Apache Maven Bundle PluginBuild-Jdk: 1.8.0_144