java 动态字节码技术和类加载

发布时间:2022-03-01 12:04:02 作者:yexindonglai@163.com 阅读(1142)

 

字节码有什么用?

1、对类的基本信息进行操作 ,可以新增、删除、修改类、属性和方法;
2、应用场景:Lombok 插件、AOP、动态修改class文件、网页上的在线执行java代码也是字节码实现的;
2、Lombok 插件就是用字节码实现,只用Data注解就可以给属性自动加上get和set方法
4、

常见的字节码操作类库:

    BCEL:  深入汇编底层语言进行类库操作;
    ASM:  轻量级字节码操作框架,直接涉及到jVM底层的操作和指令;高性能、高质量 
    CGLB: 生成类库,基于ASM实现
    javassist :开源框架,编辑和创建Java字节码的类库,我们这里主要介绍javassist


javassist 框架

    优势:比反射开销小、性能高;但是比ASM低;跟cglib差不多,使用简单。很多开源框架都在使用它。可进行修改已有方法的方法体(插入代码到已有方法体)、新增方法  、 删除方法;
    缺点:不支持数组初始化、不支持内部类和匿名类、不支持continue和break表达式、不支持泛型和枚举、某些继承关系也不支持;

 

类加载

我们的代码运行的时候,首先会将后缀为java的文件编译成后缀为 class 的文件,class文件中保存着java代码转换后的虚拟机指令,当需要使用到某个类时,虚拟机会加载它的 class 文件,并创建class对象,将class加载到虚拟机内存,这个过程称为类加载,类加载过程图如下:

我们使用一个对象的时候,在虚拟机底层已经帮我们加载好了class对象,所以我们直接用就好了;

类加载器常用方法( ClassLoader)

  • loadClass(String)

    • 当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载;

  • findClass(String)

    • 根据名称或者位置加载Class字节码,然后使用defineClass 通常由子类去实现,如果需要自定义ClassLoader的话,必须重写该方法。
  • defineClass(byte[] b, int off, int len)

    • defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象,简单地说就是把class字节码文件转化为虚拟机能识别的Class对象
  • resolveClass(Class? c)

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

 

重新加载class  ,重写 findClass 方法

刚刚我们说到,使用  findClass(String)  方法可以实现重新加载class字节码文件,因为 findClass 方法在ClassLoader里面是  protected 修饰的,只能被子类调用,而且他里面没有实现方法,只抛出一个 ClassNotFoundException 的异常

所以我们需要重写 findClass 方法,让他能被外部对象调用,并添加加载class字节码文件的逻辑代码,代码如下:

  1. package com;
  2. import java.io.InputStream;
  3. import java.net.URL;
  4. public class MyClassLoader extends ClassLoader{
  5. /**
  6. * 加载class文件
  7. * 重写此方法的目的是为了能让此方法被外部调用,父类的 findClass 是 protected 修饰的,只能被子类调用
  8. * @param name 类的全类名 示例: com.xd.User
  9. * @return
  10. * @throws ClassNotFoundException
  11. */
  12. @Override
  13. public Class<?> findClass(String name) throws ClassNotFoundException {
  14. try {
  15. // 获取class文件名称 去掉包路径
  16. String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
  17. // 获取文件输入流
  18. InputStream is = this.getClass().getResourceAsStream(fileName);
  19. // 读取字节
  20. byte[] b = new byte[is.available()];
  21. is.read(b);
  22. // 将byte字节流解析成jvm能够识别的Class对象
  23. return defineClass(name, b, 0, b.length);
  24. } catch (Exception e) {
  25. throw new ClassNotFoundException();
  26. }
  27. }
  28. }

 

使用javassist创建class字节码文件并运行

使用之前需要先导入相关的包

  1. <!-- 字节码操作框架 -->
  2. <dependency>
  3. <groupId>org.javassist</groupId>
  4. <artifactId>javassist</artifactId>
  5. <version>3.20.0-GA</version>
  6. </dependency>
  7. <!-- 处理解析json的类 -->
  8. <dependency>
  9. <groupId>com.alibaba</groupId>
  10. <artifactId>fastjson</artifactId>
  11. <version>1.2.76</version>
  12. </dependency>

1、实战,接下来我们来创建于一个class类并执行里面的方法,代码如下

  1. package com;
  2. import com.alibaba.fastjson.JSON;
  3. import javassist.*;
  4. import java.io.IOException;
  5. import java.lang.reflect.Constructor;
  6. import java.lang.reflect.InvocationTargetException;
  7. import java.lang.reflect.Method;
  8. import java.net.URL;
  9. import java.net.URLDecoder;
  10. public class MainCreate {
  11. /**
  12. * 创建class类
  13. * @param args
  14. * @throws ClassNotFoundException
  15. * @throws IOException
  16. * @throws CannotCompileException
  17. * @throws NotFoundException
  18. * @throws NoSuchMethodException
  19. * @throws IllegalAccessException
  20. * @throws InvocationTargetException
  21. * @throws InstantiationException
  22. */
  23. public static void main(String[] args) throws Exception {
  24. new MainCreate().createClass();
  25. }
  26. public void createClass() throws Exception {
  27. String className = "com.User";
  28. Class cla = this.getClass();
  29. // 获取class文件 所在绝对路径
  30. URL resource = cla.getResource("/");
  31. //获取class路径
  32. String path = URLDecoder.decode(resource.getPath(), "UTF-8");
  33. // 使用字节码技术修改class类文件信息
  34. ClassPool pool = ClassPool.getDefault(); // 从类池创建实例
  35. CtClass ctClass = pool.makeClass(className);
  36. // 创建 loginName属性
  37. CtField loginName = CtField.make("private String loginName;", ctClass);
  38. // 创建 pwd 属性
  39. CtField pwd = CtField.make("private String password;", ctClass);
  40. // 年龄属性
  41. CtField age = CtField.make("private Integer age;", ctClass);
  42. // 添加属性
  43. ctClass.addField(loginName);
  44. ctClass.addField(pwd);
  45. ctClass.addField(age);
  46. // 创建loginName的get 和 set 方法
  47. CtMethod loginSetMethod = CtMethod.make("public void setLoginName(String loginName){this.loginName = loginName;}", ctClass);
  48. CtMethod loginGetMethod = CtMethod.make("public String getLoginName(){return this.loginName;}", ctClass);
  49. // 创建password的get 和 set 方法
  50. CtMethod passwordSetMethod = CtMethod.make("public void setPassword(String password){this.password = password;}", ctClass);
  51. CtMethod passwordGetMethod = CtMethod.make("public String getPassword(){return this.password;}", ctClass);
  52. // 创建age的get 和 set 方法
  53. CtMethod ageSetMethod = CtMethod.make("public void setAge(Integer age) { this.age = age; }", ctClass);
  54. CtMethod ageGetMethod = CtMethod.make("public Integer getAge() { return age; }", ctClass);
  55. // 添加上面的get和set方法方法
  56. ctClass.addMethod(loginGetMethod);
  57. ctClass.addMethod(loginSetMethod);
  58. ctClass.addMethod(passwordSetMethod);
  59. ctClass.addMethod(passwordGetMethod);
  60. ctClass.addMethod(ageSetMethod);
  61. ctClass.addMethod(ageGetMethod);
  62. // 添加show方法,无参方法,用来打印 一些信息
  63. CtMethod showMethod = CtMethod.make("public void show(){System.out.println(\"我是用字节码创建出来的方法\");}", ctClass);
  64. ctClass.addMethod(showMethod);
  65. // 添加show方法,有参 方法
  66. CtMethod showHaveMethod = CtMethod.make("public void show(Integer num ,String value){System.out.println(\"我是用字节码创建出来的方法,num:\"+num+\",value:\"+value);}", ctClass);
  67. ctClass.addMethod(showHaveMethod);
  68. // 添加有参构造函数 , 第一个参数是int 类型,第二个和第三个参数都是string类型
  69. CtConstructor ctConstructor = new CtConstructor(
  70. new CtClass[]{
  71. pool.get("java.lang.Integer"),
  72. pool.get("java.lang.String"),
  73. pool.get("java.lang.String")},
  74. ctClass);
  75. // 创建构造函数的方法主题内容,就是 {}里面的内容 $n 表示第n个参数
  76. ctConstructor.setBody("{this.age = $1;this.loginName = $2; this.password = $3;}");
  77. //添加构造器
  78. ctClass.addConstructor(ctConstructor);
  79. // 添加无参构造函数
  80. CtConstructor defaultConstructor = new CtConstructor(
  81. new CtClass[]{},
  82. ctClass);
  83. // 创建构造函数的方法主题内容,就是 {}里面的内容
  84. defaultConstructor.setBody("{}");
  85. //添加构造器
  86. ctClass.addConstructor(defaultConstructor);
  87. // 获取类路径
  88. String classFilePath = path;
  89. System.out.println("classFilePath: " + classFilePath);
  90. // 将类写入到 指定文件夹
  91. ctClass.writeFile(classFilePath);
  92. // 重新加载class
  93. MyClassLoader classLoader = new MyClassLoader();
  94. Class<?> userClass = classLoader.findClass(className);
  95. // 使用无参构造函数创建实例
  96. // Constructor constructor = userClass.getConstructor();
  97. // Object o = constructor.newInstance();
  98. // 执行有参数构造函数创建实例
  99. Constructor constructor = userClass.getConstructor(Integer.class, String.class, String.class);
  100. // 创建实例;执行有参构造函数
  101. Object o = constructor.newInstance(1, "user", "pwd");
  102. System.out.println(JSON.toJSONString(o));
  103. // 执行无参show方法
  104. Method show = userClass.getDeclaredMethod("show");
  105. show.invoke(o);
  106. //执行有参show方法
  107. Method showHave = userClass.getDeclaredMethod("show", Integer.class, String.class);
  108. showHave.invoke(o,2,"hello");
  109. }
  110. }

 

2、创建出来的class文件通过反编译后内容如下

3、运行main后打印结果如下

 

使用javassist修改class字节码文件并执行

1、动态加载技术其实就是通过修改class文件并且热部署执行的, 我们熟悉的热部署 就是这么实现的,好吧,说的有点懵,还是接上代码吧:

UserUpdate.java (需要修改的类)

  1. package org;
  2. public class UserUpdate {
  3. public void show(){
  4. System.out.println("我是版本1.0");
  5. }
  6. }

 

MainUpdate.java 

  1. package org;
  2. import com.MyClassLoader;
  3. import com.User;
  4. import com.alibaba.fastjson.JSON;
  5. import com.sun.xml.internal.ws.api.message.ExceptionHasMessage;
  6. import javassist.*;
  7. import java.io.File;
  8. import java.io.IOException;
  9. import java.lang.reflect.Constructor;
  10. import java.lang.reflect.InvocationTargetException;
  11. import java.lang.reflect.Method;
  12. import java.net.URL;
  13. import java.net.URLDecoder;
  14. public class MainUpdate {
  15. /**
  16. * 动态修改class类
  17. * @param args
  18. * @throws ClassNotFoundException
  19. * @throws IOException
  20. * @throws CannotCompileException
  21. * @throws NotFoundException
  22. * @throws NoSuchMethodException
  23. * @throws IllegalAccessException
  24. * @throws InvocationTargetException
  25. * @throws InstantiationException
  26. */
  27. public static void main(String[] args) throws Exception {
  28. new MainUpdate().updateClass();
  29. }
  30. /**
  31. * 修改class文件
  32. * @throws Exception
  33. */
  34. public void updateClass() throws Exception {
  35. String className = "org.UserUpdate";
  36. Class<?> cla = this.getClass();
  37. // 获取class文件 所在绝对路径
  38. URL resource = cla.getResource("/");
  39. //获取class路径
  40. String path = URLDecoder.decode(resource.getPath(), "UTF-8");
  41. // 使用字节码技术修改class类文件信息
  42. ClassPool pool = ClassPool.getDefault(); // 从类池创建实例
  43. // TODO 关键代码,读取全类名编译后的 class文件
  44. CtClass ctClass = pool.get(className);
  45. // 创建 loginName属性
  46. CtField loginName = CtField.make("private String loginName;", ctClass);
  47. // 创建 pwd 属性
  48. CtField pwd = CtField.make("private String password;", ctClass);
  49. // 创建 age属性
  50. CtField age = CtField.make("private Integer age;", ctClass);
  51. // 添加属性
  52. ctClass.addField(loginName);
  53. ctClass.addField(pwd);
  54. ctClass.addField(age);
  55. // 创建loginName的get 和 set 方法
  56. CtMethod loginSetMethod = CtMethod.make("public void setLoginName(String loginName){this.loginName = loginName;}", ctClass);
  57. CtMethod loginGetMethod = CtMethod.make("public String getLoginName(){return this.loginName;}", ctClass);
  58. // 创建password的get 和 set 方法
  59. CtMethod passwordSetMethod = CtMethod.make("public void setPassword(String password){this.password = password;}", ctClass);
  60. CtMethod passwordGetMethod = CtMethod.make("public String getPassword(){return this.password;}", ctClass);
  61. // 创建age的get 和 set 方法
  62. CtMethod ageSetMethod = CtMethod.make("public void setAge(Integer age) { this.age = age; }", ctClass);
  63. CtMethod ageGetMethod = CtMethod.make("public Integer getAge() { return age; }", ctClass);
  64. // 添加方法
  65. ctClass.addMethod(loginGetMethod);
  66. ctClass.addMethod(loginSetMethod);
  67. ctClass.addMethod(passwordSetMethod);
  68. ctClass.addMethod(passwordGetMethod);
  69. ctClass.addMethod(ageSetMethod);
  70. ctClass.addMethod(ageGetMethod);
  71. // 添加构造函数 , 第一个参数是int 类型,第二个和第三个参数都是string类型
  72. CtConstructor ctConstructor = new CtConstructor(
  73. new CtClass[]{
  74. pool.get("java.lang.Integer"),
  75. pool.get("java.lang.String"),
  76. pool.get("java.lang.String")},
  77. ctClass);
  78. // 创建构造函数的方法主题内容,就是 {}里面的内容 $n 表示第n个参数
  79. ctConstructor.setBody("{this.age = $1;this.loginName = $2; this.password = $3;}");
  80. //添加构造器
  81. ctClass.addConstructor(ctConstructor);
  82. // 添加构造函数 ,无参数构造函数 TODO 因为class类里面已经有一个默认的无参构造函数了,所以不需要再次添加,否则会报错
  83. // CtConstructor defaultConstructor = new CtConstructor(
  84. // new CtClass[]{},
  85. // ctClass);
  86. // // 创建构造函数的方法主题内容,就是 {}里面的内容
  87. // defaultConstructor.setBody("{}");
  88. // //在添加默认构造器
  89. // ctClass.addConstructor(defaultConstructor);
  90. // 获取类路径
  91. String classFilePath = path;
  92. System.out.println("classFilePath: " + classFilePath);
  93. // 将类写入到 指定文件夹
  94. ctClass.writeFile(classFilePath);
  95. // 重新加载class
  96. MyClassLoader classLoader = new MyClassLoader();
  97. Class<?> userClass = classLoader.loadClass(className);
  98. // 使用无参构造函数创建实例
  99. // Constructor constructor = userClass.getConstructor();
  100. // Object o = constructor.newInstance();
  101. // 执行有参数构造函数创建实例
  102. Constructor constructor = userClass.getConstructor(Integer.class, String.class, String.class);
  103. // 创建实例;执行有参构造函数
  104. Object o = constructor.newInstance(1, "user", "pwd");
  105. UserUpdate user = (UserUpdate) o;
  106. System.out.println(JSON.toJSONString(user));
  107. System.out.println(JSON.toJSONString(o));
  108. // getDeclaredMethod 获取当前类的show方法
  109. Method show = userClass.getDeclaredMethod("show");
  110. show.invoke(o);
  111. }
  112. }

2、运行后,我们可以看到修改后的class文件通过反编译后内容如下

3、控制台输出内容如下

关键字Java