字节码技术---------动态代理,lombok插件底层原理。类加载器

字节码技术应用场景

AOP技术、Lombok去除重复代码插件、动态修改class文件等java

 

字节技术优点

   Java字节码加强指的是在Java字节码生成以后,对其进行修改,加强其功能,这种方式至关于对应用程序的二进制文件进行修改。Java字节码加强主要是为了减小冗余代码,提升性能等。api

实现字节码加强的主要步骤为:数组

1、修改字节码缓存

   在内存中获取到原来的字节码,而后经过一些工具(如 ASM,Javaasist)来修改它的byte[]数组,获得一个新的byte数组。安全

   2、使修改后的字节码生效服务器

    有两种方法:网络

  1) 自定义ClassLoader来加载修改后的字节码;数据结构

   2)替换掉原来的字节码:在JVM加载用户的Class时,拦截,返回修改后的字节码;或者在运行时,使用Instrumentation.redefineClasses方法来替换掉原来的字节码多线程

常见的字节码操做类库

 

BCEL

Byte Code Engineering Library(BCEL),这是Apache Software Foundation的Jakarta项目的一部分。BCEL是Java classworking 普遍使用的一种框架,它可让您深刻jvm汇编语言进行类库操做的细节。BCEL与javassist有不一样的处理字节码方法,BCEL在实际的jvm指令层次上进行操做(BCEL拥有丰富的jvm指令集支持) 而javassist所强调的是源代码级别的工做。框架

ASM

是一个轻量级Java字节码操做框架,直接涉及到JVM底层的操做和指令

高性能,高质量

CGLB

 生成类库,基于ASM实现

javassist

是一个开源的分析,编辑和建立Java字节码的类库。性能较ASM差,跟cglib差很少,可是使用简单。不少开源框架都在使用它。

Javassist优点

 

– 比反射开销小,性能高。

–javassist性能高于反射,低于ASM

运行时操做字节码可让咱们实现以下功能:

– 动态生成 新的类

– 动态改变某个类的结构 ( 添加 / 删除 / 修改    新的属性 / 方法 )

javassist 的最外层的 API 和 JAVA 的反射包中的 API 颇为 相似 。

它 主要 由 CtClass , CtMethod, ,以及 CtField 几个类组成。用以执行和 JDK 反射 API 中 java.lang.Class, java.lang.reflect.Method, java.lang.reflect.Method .Field 相同的 操做 。

方法操做

– 修改已有方法的方法体(插入代码到已有方法体)

– 新增方法   删除方法

javassist的局限性

JDK5.0 新语法不支持 ( 包括泛型、枚举 ) ,不支持注解修改,但能够经过底层的 javassist 类来解决,具体参考: javassist.bytecode.annotation
不支持数组的初始化,如 String[]{"1","2"} ,除非只有数组的容量为 1
不支持内部类和匿名类
不支持 continue 和 break表达式。
对于继承关系,有些不支持。例如
class A {}  
class B extends A {} 
class C extends B {} 

使用Javassist建立类

public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException { Class<?> clazz = Class.forName("com.itmayiedu.Test0005"); Object newInstance = clazz.newInstance(); Method method = clazz.getDeclaredMethod("sum", int.class, int.class); Object invoke = method.invoke(newInstance, 1, 1); } public void sum(int a, int b) { System.out.println("sum:" + a + b); } public static void main(String[] args) throws CannotCompileException, NotFoundException, IOException { ClassPool pool = ClassPool.getDefault(); // 建立class文件
        CtClass userClass = pool.makeClass("com.itmayiedu.entity.User"); // 建立id属性
        CtField idField = CtField.make("private Integer id;", userClass); // 建立name属性
        CtField nameField = CtField.make("private Integer name;", userClass); // 添加属性
 userClass.addField(idField); // 添加属性
 userClass.addField(nameField); // 建立方法
        CtMethod getIdMethod = CtMethod.make("public Integer getId() {return id;}", userClass); // 建立方法
        CtMethod setIdMethod = CtMethod.make("public void setId(Integer id) { this.id = id; }", userClass); // 添加方法
 userClass.addMethod(getIdMethod); // 添加方法
 userClass.addMethod(setIdMethod); // 添加构造器
        CtConstructor ctConstructor = new CtConstructor(new CtClass[] { CtClass.intType, pool.get("java.lang.String") }, userClass); // 建立Body
        ctConstructor.setBody("    {this.id = id;this.name = name;}"); userClass.addConstructor(ctConstructor); userClass.writeFile("F:/test");// 将构造好的类写入到F:\test 目录下
    }

使用Javassist修改类文件信息

public static void main(String[] args) throws NotFoundException, CannotCompileException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException, IOException { ClassPool pool = ClassPool.getDefault(); // 须要加载类信息
        CtClass userClass = pool.get("com.itmayiedu.User"); // 须要添加的方法
        CtMethod m = new CtMethod(CtClass.intType, "add", new CtClass[] { CtClass.intType, CtClass.intType }, userClass); // 方法权限
 m.setModifiers(Modifier.PUBLIC); // 方法体内容
        m.setBody("{System.out.println(\"Test003\"); return $1+$2;}"); userClass.addMethod(m); userClass.writeFile("F:/test");// 将构造好的类写入到F:\test 目录下 // 使用反射技术执行方法
        Class clazz = userClass.toClass(); Object obj = clazz.newInstance(); // 经过调用User 无参构造函数
        Method method = clazz.getDeclaredMethod("add", int.class, int.class); Object result = method.invoke(obj, 200, 300); System.out.println(result); }

 

热部署的原理是什么

想要知道热部署的原理,必需要了解java类的加载过程。一个java类文件到虚拟机里的对象,要通过以下过程。

 

首先经过java编译器,将java文件编译成class字节码,类加载器读取class字节码,再将类转化为实例,对实例newInstance就能够生成对象。

类加载器ClassLoader功能,也就是将class字节码转换到类的实例。

java应用中,全部的实例都是由类加载器,加载而来。

通常在系统中,类的加载都是由系统自带的类加载器完成,并且对于同一个全限定名的java类(如com.csiar.soc.HelloWorld),只能被加载一次,并且没法被卸载。

这个时候问题就来了,若是咱们但愿将java类卸载,而且替换更新版本的java类,该怎么作呢?

     既然在类加载器中,java类只能被加载一次,而且没法卸载。那是否是能够直接把类加载器给换了?答案是能够的,咱们能够自定义类加载器,并重写ClassLoader的findClass方法。想要实现热部署能够分如下三个步骤:

一、销毁该自定义ClassLoader

二、更新class类文件

三、建立新的ClassLoader去加载更新后的class类文件。

 

类加载的机制的层次结构

每一个编写的”.java”拓展名类文件都存储着须要执行的程序逻辑,这些”.java”文件通过Java编译器编译成拓展名为”.class”的文件,”.class”文件中保存着Java代码经转换后的虚拟机指令,当须要使用某个类时,虚拟机将会加载它的”.class”文件,并建立对应的class对象,将class文件加载到虚拟机的内存,这个过程称为类加载,这里咱们须要了解一下类加载的过程,以下:

Jvm执行class文件

 

 

步骤1、类加载机制

将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个表明这个类的java.lang.Class对象,做为方法区类数据的访问入口,这个过程须要类加载器参与。

当系统运行时,类加载器将.class文件的二进制数据从外部存储器(如光盘,硬盘)调入内存中,CPU再从内存中读取指令和数据进行运算,并将运算结果存入内存中。内存在该过程当中充当着"二传手"的做用,通俗的讲,若是没有内存,类加载器从外部存储设备调入.class文件二进制数据直接给CPU处理,而因为CPU的处理速度远远大于调入数据的速度,容易形成数据的脱节,因此须要内存起缓冲做用。

类将.class文件加载至运行时的方法区后,会在堆中建立一个Java.lang.Class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程当中建立的,每一个类都对应有一个Class类型的对象,Class类的构造方法是私有的,只有JVM可以建立。所以Class对象是反射的入口,使用该对象就能够得到目标类所关联的.class文件中具体的数据结构。

 

类加载的最终产物就是位于堆中的Class对象(注意不是目标类对象),该对象封装了类在方法区中的数据结构,而且向用户提供了访问方法区数据结构的接口,即Java反射的接口。

 

 

步骤2、链接过程

将java类的二进制代码合并到JVM的运行状态之中的过程

验证:确保加载的类信息符合JVM规范,没有安全方面的问题

准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配

解析:虚拟机常量池的符号引用替换为字节引用过程

步骤3、初始化

初始化阶段是执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译器自动收藏类中的全部类变量的赋值动做和静态语句块(static块)中的语句合并产生,代码从上往下执行。

当初始化一个类的时候,若是发现其父类尚未进行过初始化,则须要先触发其父类的初始化

虚拟机会保、、、证一个类的<clinit>()方法在多线程环境中被正确加锁和同步

当范围一个Java类的静态域时,只有真正声名这个域的类才会被初始化

 

 

类加载器的层次结构

 

启动(Bootstrap)类加载器

扩展(Extension)类加载器

系统(-)类加载器

 

 

启动(Bootstrap)类加载器

启动类加载器主要加载的是JVM自身须要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必因为虚拟机是按照文件名识别加载jar包的,如rt.jar,若是文件名不被虚拟机识别,即便把jar包丢到lib目录下也是没有做用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

扩展(Extension)类加载器

扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者能够直接使用标准扩展类加载器。

系统(System)类加载器、

也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是咱们常常用到的classpath路径,开发者能够直接使用系统类加载器,通常状况下该类加载是程序中默认的类加载器,经过ClassLoader#getSystemClassLoader()方法能够获取到该类加载器。

  在Java的平常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,咱们还能够自定义类加载器,须要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当须要使用该类时才会将它的class文件加载到内存生成class对象,并且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式,下面咱们进一步了解它。

理解双亲委派模式

采用双亲委派模式的是好处是Java类随着它的类加载器一块儿具有了一种带有优先级的层次关系,经过这种层级关能够避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设经过网络传递一个名为java.lang.Integer的类,经过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会从新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样即可以防止核心API库被随意篡改。可能你会想,若是咱们在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,通过双亲委托模式,传递到启动类加载器中,因为父类加载器路径下并无该类,因此不会加载,将反向委托给子类加载器加载,最终会经过系统类加载器加载该类。可是这样作是不容许,由于java.lang是核心API包,须要访问权限,强制加载将会报出以下异常

java.lang.SecurityException: Prohibited package name: java.lang

 

因此不管如何都没法加载成功的。下面咱们从代码层面了解几个Java中定义的类加载器及其双亲委派模式的实现,它们类图关系以下

 

 

双亲委派模式是在Java 1.2后引入的,其工做原理的是,若是一个类加载器收到了类加载请求,它并不会本身先去加载,而是把这个请求委托给父类的加载器去执行,若是父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,若是父类加载器能够完成类加载任务,就成功返回,假若父类加载器没法完成此加载任务,子加载器才会尝试本身去加载,这就是双亲委派模式,即每一个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子本身想办法去完成,这不就是传说中的实力坑爹啊?那么采用这种模式有啥用呢?

双亲委派模式优点

采用双亲委派模式的是好处是Java类随着它的类加载器一块儿具有了一种带有优先级的层次关系,经过这种层级关能够避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设经过网络传递一个名为java.lang.Integer的类,经过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会从新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样即可以防止核心API库被随意篡改。可能你会想,若是咱们在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,通过双亲委托模式,传递到启动类加载器中,因为父类加载器路径下并无该类,因此不会加载,将反向委托给子类加载器加载,最终会经过系统类加载器加载该类。可是这样作是不容许,由于java.lang是核心API包,须要访问权限,强制加载将会报出以下异常

类加载器间的关系

咱们进一步了解类加载器间的关系(并不是指继承关系),主要能够分为如下4点

启动类加载器,由C++实现,没有父类。

拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null

系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader

自定义类加载器,父类加载器确定为AppClassLoader。

类加载器经常使用方法

loadClass(String)

该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2以后再也不建议用户重写但用户能够直接调用该方法,loadClass()方法是ClassLoader类本身实现的,该方法中的逻辑就是双亲委派模式的实现,其源码以下,loadClass(String name, boolean resolve)是一个重载方法,resolve参数表明是否生成class对象的同时进行解析相关操做。

正如loadClass方法所展现的,当类加载请求到来时,先从缓存中查找该类对象,若是存在直接返回,若是不存在则交给该类加载去的父加载器去加载,假若没有父加载则交给顶级启动类加载器去加载,最后假若仍没有找到,则使用findClass()方法去加载(关于findClass()稍后会进一步介绍)。从loadClass实现也能够知道若是不想从新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载本身指定的类,那么咱们能够直接使用this.getClass().getClassLoder.loadClass("className"),这样就能够直接调用ClassLoader的loadClass方法获取到class对象。

findClass(String)

在JDK1.2以前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类,可是在JDK1.2以后已再也不建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用本身的findClass()方法来完成类加载,这样就能够保证自定义的类加载器也符合双亲委托模式。须要注意的是ClassLoader类中并无实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法一般是和defineClass方法一块儿使用的(稍后会分析)

defineClass(byte[] b, int off, int len)

defineClass()方法是用来将byte字节流解析成JVM可以识别的Class对象(ClassLoader中已实现该方法逻辑),经过这个方法不只可以经过class文件实例化class对象,也能够经过其余方式实例化class对象,如经过网络接收一个类的字节码,而后转换为byte字节流建立对应的Class对象,defineClass()方法一般与findClass()方法一块儿使用,通常状况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,而后调用defineClass()方法生成类的Class对象

resolveClass(Class≺?≻ c)

使用该方法可使用类的Class对象建立完成也同时被解析。前面咱们说连接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。

热部署

 

对于Java应用程序来讲,热部署就是在运行时更新Java类文件。

 

热部署的原理是什么

想要知道热部署的原理,必需要了解java类的加载过程。一个java类文件到虚拟机里的对象,要通过以下过程。

 

首先经过java编译器,将java文件编译成class字节码,类加载器读取class字节码,再将类转化为实例,对实例newInstance就能够生成对象。

类加载器ClassLoader功能,也就是将class字节码转换到类的实例。

在java应用中,全部的实例都是由类加载器,加载而来。

通常在系统中,类的加载都是由系统自带的类加载器完成,并且对于同一个全限定名的java类(如com.csiar.soc.HelloWorld),只能被加载一次,并且没法被卸载。

这个时候问题就来了,若是咱们但愿将java类卸载,而且替换更新版本的java类,该怎么作呢?

     既然在类加载器中,java类只能被加载一次,而且没法卸载。那是否是能够直接把类加载器给换了?答案是能够的,咱们能够自定义类加载器,并重写ClassLoader的findClass方法。想要实现热部署能够分如下三个步骤:

一、销毁该自定义ClassLoader

二、更新class类文件

三、建立新的ClassLoader去加载更新后的class类文件。

热部署与热加载

Java热部署与Java热加载的联系和区别

Java热部署与热加载的联系

1.不重启服务器编译/部署项目

2.基于Java的类加载器实现

 Java热部署与热加载的区别

部署方式

热部署在服务器运行时从新部署项目

热加载在运行时从新加载class

 实现原理

热部署直接从新加载整个应用

热加载在运行时从新加载class

 使用场景

热部署更多的是在生产环境使用

热加载则更多的实在开发环境使用

 

相关代码

 

User没有被修改类

public class User {

 

      public void add() {

           System.out.println("addV1,没有修改过...");

      }

 

}

 

User更新类

public class User {

 

      public void add() {

           System.out.println("我把以前的user add方法修改啦!");

      }

 

}

 

 

自定义类加载器

public class MyClassLoader extends ClassLoader {

 

      @Override

      protected Class<?> findClass(String name) throws ClassNotFoundException {

           try {

                 // 文件名称

                 String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";

                 // 获取文件输入流

                 InputStream is = this.getClass().getResourceAsStream(fileName);

                 // 读取字节

                 byte[] b = new byte[is.available()];

                 is.read(b);

                 // 将byte字节流解析成jvm可以识别的Class对象

                 return defineClass(name, b, 0, b.length);

           } catch (Exception e) {

                 throw new ClassNotFoundException();

           }

 

      }

 

}

 

 

更新代码

 

public class Hotswap {

 

     public static void main(String[] args)

              throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException,

              SecurityException, IllegalArgumentException, InvocationTargetException, InterruptedException {

         loadUser();

         System.gc();

         Thread.sleep(1000);// 等待资源回收

         // 须要被热部署的class文件

         File file1 = new File("F:\\test\\User.class");

         // 以前编译好的class文件

         File file2 = new File(

                   "F:\\itmayiedujiangke2018-02-24\\itmayiedu_itmayiedu_day_17\\target\\classes\\com\\itmayiedu\\User.class");

         boolean isDelete = file2.delete();// 删除旧版本的class文件

         if (!isDelete) {

              System.out.println("热部署失败.");

              return;

         }

         file1.renameTo(file2);

         System.out.println("update success!");

         loadUser();

     }

 

     public static void loadUser() throws ClassNotFoundException, InstantiationException, IllegalAccessException,

              NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {

         MyClassLoader myLoader = new MyClassLoader();

         Class<?> class1 = myLoader.findClass("com.itmayiedu.User");

         Object obj1 = class1.newInstance();

         Method method = class1.getMethod("add");

         method.invoke(obj1);

         System.out.println(obj1.getClass());

         System.out.println(obj1.getClass().getClassLoader());

     }

}

相关文章
相关标签/搜索