java 动态加载 class cglib

Java动态加载Class Cglib

引言

在 Java 开发中,有时我们需要在运行时动态地加载类,并通过反射机制调用类的方法。这种动态加载类的需求常常出现在框架、插件系统以及动态代理等场景中。而 Cglib 是一个强大的 Java 字节码增强库,可以在运行时动态生成一个子类来扩展一个类的功能。本文将介绍 Java 动态加载 Class 的基本概念和使用 Cglib 实现动态加载的方法。

什么是动态加载Class

动态加载 Class 指的是在程序运行过程中,根据需要动态地加载类,并在运行时使用这些类。Java 提供了反射机制来实现动态加载 Class,通过 Class 类的 forName 方法可以根据类名获取对应的 Class 对象,然后通过 Class 对象可以实例化对象、调用方法等。

动态加载 Class 的主要优点是可以根据需要加载指定的类,而不需要在编译期间就确定加载的类。这样可以在运行时动态地扩展程序的功能,使程序更加灵活和可扩展。

使用Cglib实现动态加载Class

Cglib 是一个基于 ASM 的字节码增强库,可以在运行时动态生成一个子类来扩展一个类的功能。下面我们将通过一个示例来演示如何使用 Cglib 实现动态加载 Class。

示例

假设我们有一个接口 UserService 和一个实现类 UserServiceImpl,我们想要在运行时动态地扩展 UserServiceImpl 的功能。首先,我们需要引入Cglib库的依赖。

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>
 

然后,我们定义一个类 MethodInterceptorImpl,实现 MethodInterceptor 接口,用于拦截方法调用并增强方法的功能。

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class MethodInterceptorImpl implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("Before method: " + method.getName());
        
        // 增强方法的功能
        System.out.println("Enhanced method logic");
        
        Object result = proxy.invokeSuper(obj, args);
        
        System.out.println("After method: " + method.getName());
        return result;
    }
}
 

接下来,我们使用Cglib动态加载 UserServiceImpl 类,并增强其功能。

import net.sf.cglib.proxy.Enhancer;

public class DynamicLoaderDemo {

    public static void main(String[] args) {
        // 创建Enhancer对象
        Enhancer enhancer = new Enhancer();
        // 设置被代理类的父类
        enhancer.setSuperclass(UserServiceImpl.class);
        // 设置回调对象
        enhancer.setCallback(new MethodInterceptorImpl());
        // 创建代理对象
        UserServiceImpl proxy = (UserServiceImpl) enhancer.create();
        // 调用代理对象的方法
        proxy.createUser();
    }
}
 

运行上述代码,输出结果如下:

Before method: createUser
Enhanced method logic
Creating user...
After method: createUser
 

从输出结果可以看出,我们成功地使用Cglib实现了动态加载 UserServiceImpl 类,并在调用 createUser 方法时增强了其功能。

流程图

下面是使用 mermaid 语法绘制的动态加载 Class 的流程图。

 

开始创建 Enhancer 对象设置被代理类的父类设置回调对象创建代理对象调用代理对象的方法结束

总结

本文介绍了 Java 动态加载 Class 的概念和使用 Cglib 实现动态加载的方法。动态加载 Class 可以在程序运行时根据需要加载指定的类,从而实现程序的动态扩展。Cglib 是一个强大的 Java 字节码增强库,可以在运行时动态生成一个子类来扩展一个类的功能。

classloader_百度百科

Java Class 卸载与 ClassLoader ,class 热替换_51CTO 博客_class.forname 和 classloader 区别

JVM 中的 Class 只有满足以下三个条件,才能被 GC 回收,也就是该 Class 被卸载(unload):

   - 该类所有的实例都已经被 GC,也就是 JVM 中不存在该 Class 的任何实例。
   - 加载该类的 ClassLoader 已经被 GC。
   - 该类的 java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法

 

jsp 和 java 类是完全不一样的概念。

jsp->servlet 在 web 容器中,你的 servlet 是单例的,也是无状态的,线程安全的。也就是只有一个对象,

jsp 改变以后,web 容器只要把相应的 servlet 对象更新就好了。

而 java 呢?

可能这个类在你的应用中有 n 个实例,与这些实例单向,双向关联的又有 n 个实例。如果你修改了,这些 jvm 存在的老的实例对象怎么办????

java 这类静态语言无法实现象 asp,php,jsp 的效果的。

 weblogic 热部署原理

Weblogic 允许在 wls 运行时部署组件的新版本。这个过程被称作热部署。因为 java classloader 没有任何一种机制来卸下一系列存在的类,也不能用类的新版本来替换老版本,为了在一个运行的虚拟机中更新相关的类,classloader 必须被替换掉。当它被替换时,它所装载的所有类以及衍生的子 classloader 也要被重新装载。这些类的所有实例也必需被重新装载。在 wls 中,每一个应用组件都有一个层次化的 classloaders,它们都是 system classloader 的子类,这种结构有助于每个应用或应用的一部分能被单独重新加载,而不会影响其它的组件。

 

类加载器的种类:

  1. Bootstrap ClassLoader / 启动类加载器 
    主要负责 jdk_home/lib 目录下的核心 api 或 -Xbootclasspath 选项指定的 jar 包装入工作。
  2. Extension ClassLoader / 扩展类加载器 
    主要负责 jdk_home/lib/ext 目录下的 jar 包或 -Djava.ext.dirs 指定目录下的 jar 包装入工作。
  3. System ClassLoader / 系统类加载器 
    主要负责 java -classpath/-Djava.class.path 所指的目录下的类与 jar 包装入工作。
  4. User Custom ClassLoader / 用户自定义类加载器 (java.lang.ClassLoader 的子类) 
    在程序运行期间,通过 java.lang.ClassLoader 的子类动态加载 class 文件,体现 java 动态实时类装入特性。

类加载器的特性:

 

  1. 每个 ClassLoader 都维护了一份自己的名称空间, 同一个名称空间里不能出现两个同名的类。
  2. 为了实现 java 安全沙箱模型顶层的类加载器安全机制,java 默认采用了 "双亲委派的加载链" 结构。

 

 

自定义类加载器加载一个类的步骤

classloader-load-class
 
ClassLoader 类加载逻辑分析, 以下逻辑是除 BootstrapClassLoader 外的类加载器加载流程:
// 检查类是否已被装载过        
Class c = findLoadedClass(name);        
if (c == null ) {        
         // 指定类未被装载过        
         try {        
                 if (parent != null ) {        
                         // 如果父类加载器不为空, 则委派给父类加载        
                         c = parent.loadClass(name, false );        
                 } else {        
                         // 如果父类加载器为空, 则委派给启动类加载加载        
                         c = findBootstrapClass0(name);        
                 }        
         } catch (ClassNotFoundException e) {        
                 // 启动类加载器或父类加载器抛出异常后, 当前类加载器将其        
                 // 捕获, 并通过findClass方法, 由自身加载        
                 c = findClass(name);        
         }        
}
 

 

线程上下文类加载器

java 默认的线程上下文类加载器是 系统类加载器 (AppClassLoader)。

// Now create the class loader to use to launch the application        
try {        
        loader = AppClassLoader.getAppClassLoader(extcl);        
} catch (IOException e) {        
        throw new InternalError(        
"Could not create application class loader" );        
}         
     
// Also set the context class loader for the primordial thread.        
Thread.currentThread().setContextClassLoader(loader);
 

以上代码摘自 sun.misc.Launch 的无参构造函数 Launch ()。

使用线程上下文类加载器,可以在执行线程中,抛弃双亲委派加载链模式,使用线程上下文里的类加载器加载类.

典型的例子有,通过线程上下文来加载第三方库 jndi 实现,而不依赖于双亲委派.

大部分 java app 服务器 (jboss, tomcat..) 也是采用 contextClassLoader 来处理 web 服务。

还有一些采用 hotswap 特性的框架,也使用了线程上下文类加载器,比如 seasar (full stack framework in japenese).

线程上下文从根本解决了一般应用不能违背双亲委派模式的问题.

使 java 类加载体系显得更灵活.

随着多核时代的来临,相信多线程开发将会越来越多地进入程序员的实际编码过程中。因此,

在编写基础设施时, 通过使用线程上下文来加载类,应该是一个很好的选择。

当然,好东西都有利弊。使用线程上下文加载类,也要注意,保证多根需要通信的线程间的类加载器应该是同一个,

防止因为不同的类加载器,导致类型转换异常 (ClassCastException)。

 

三.命名空间及其作用
每个类装载器有自己的命名空间,命名空间由所有以此装载器为创始类装载器的类组成。不同命名空间的两个类是不可见的,但只要得到类所对应的 Class 对象的 reference,还是可以访问另一命名空间的类。
 
例 2 演示了一个命名空间的类如何使用另一命名空间的类。在例子中,LoaderSample2 由系统类装载器装载,LoaderSample3 由自定义的装载器 loader 负责装载,两个类不在同一命名空间,但 LoaderSample2 得到了 LoaderSample3 所对应的 Class 对象的 reference,所以它可以访问 LoaderSampl3 中公共的成员 (如 age)。
例 2 不同命名空间的类的访问

/*LoaderSample2.java*/
import    java.net. * ; 
import    java.lang.reflect. * ; 
public     class    LoaderSample2 { 
         public     static     void    main(String[] args) { 
                 try    { 
                        String path    =    System.getProperty( " user.dir " ); 
                        URL[] us    =    { new    URL( " file:// "     +    path    +     " /sub/ " )}; 
                        ClassLoader loader    =     new    URLClassLoader(us); 
                        Class c    =    loader.loadClass( " LoaderSample3 " ); 
                        Object o    =    c.newInstance(); 
                        Field f    =    c.getField( " age " ); 
                         int    age    =    f.getInt(o); 
                        System.out.println( " age is    "     +    age); 
                }    catch    (Exception e) { 
                        e.printStackTrace(); 
                } 
        } 
}
/*sub/Loadersample3.java*/
public     class    LoaderSample3 { 
         static    { 
                System.out.println( " LoaderSample3 loaded " ); 
        } 
         public     int    age    =     30 ; 
}
 

编译:javac LoaderSample2.java; javac sub/LoaderSample3.java

运行:java LoaderSample2

LoaderSample3 loaded

age is 30

从运行结果中可以看出,在类 LoaderSample2 中可以创建处于另一命名空间的类 LoaderSample3 中的对象并可以访问其公共成员 age。

 

说明:如果 LoaderSample3 在 classpath 下能够找到,则由 URLClassLoader 的 parent loader AppClassLoader 来加载,如果不在 classpath 下

则由 URLClassLoader 自己加载,即 LoaderSample3.getClass ().getClassLoader () 是 URLClassLoader

运行时包 (runtime package)

由同一类装载器定义装载的属于相同包的类组成了运行时包,决定两个类是不是属于同一个运行时包,不仅要看它们的包名是否相同,还要看的定义类装载器是否相同。只有属于同一运行时包的类才能互相访问包可见的类和成员。这样的限制避免了用户自己的代码冒充核心类库的类访问核心类库包可见成员的情况。假设用户自己定义了一个类 java.lang.Yes,并用用户自定义的类装载器装载,由于 java.lang.Yes 和核心类库 java.lang.* 由不同的装载器装载,它们属于不同的运行时包,所以 java.lang.Yes 不能访问核心类库 java.lang 中类的包可见的成员。 

 

总结
命名空间并没有完全禁止属于不同空间的类的互相访问,双亲委托模型加强了 Java 的安全,运行时包增加了对包可见成员的保护。
 
二.    扩展 ClassLoader 方法
我们目的是从本地文件系统使用我们实现的类装载器装载一个类。为了创建自己的类装载器我们应该扩展 ClassLoader 类,这是一个抽象类。我们创建一个 FileClassLoader extends ClassLoader。我们需要覆盖 ClassLoader 中的 findClass (String name) 方法,这个方法通过类的名字而得到一个 Class 对象。

public    Class findClass(String name) 
        { 
                 byte [] data    =    loadClassData(name); 
                 return    defineClass(name, data,    0 , data.length); 
        }
 

我们还应该提供一个方法 loadClassData (String name),通过类的名称返回 class 文件的字 
节数组。然后使用 ClassLoader 提供的 defineClass () 方法我们就可以返回 Class 对象了。

public     byte [] loadClassData(String name) 
        { 
                FileInputStream fis    =     null ; 
                 byte [] data    =     null ; 
                 try    
                { 
                        fis    =     new    FileInputStream( new    File(drive    +    name    +    fileType)); 
                        ByteArrayOutputStream baos    =     new    ByteArrayOutputStream(); 
                         int    ch    =     0 ; 
                         while    ((ch    =    fis.read())    !=     - 1 ) 
                        { 
                                baos.write(ch); 
                                
                        } 
                        data    =    baos.toByteArray(); 
                }    catch    (IOException e) 
                { 
                        e.printStackTrace(); 
                } 
                 
                 return    data; 
        }

三 Tomcat 中关于类的加载与卸载
        Tomcat 中与其说有热加载,还不如说是热部署来的准确些。因为对于一个应用,其中 class 文件被修改过,那么 Tomcat 会先卸载这个应用 (Context),然后重新加载这个应用,其中关键就在于自定义 ClassLoader 的应用。这里有篇文章很好的介绍了 tomcat 中对于 ClassLoader 的应用,请点击 here

Tomcat 启动的时候,ClassLoader 加载的流程:
1 Tomcat 启动的时候,用 system classloader 即 AppClassLoader 加载 {catalina.home}/bin 里面的 jar 包,也就是 tomcat 启动相关的 jar 包。
2 Tomcat 启动类 Bootstrap 中有 3 个 classloader 属性,catalinaLoader、commonLoader、sharedLoader 在 Tomcat7 中默认他们初始化都为同一个 StandardClassLoader 实例。具体的也可以在 {catalina.home}/bin/bootstrap.jar 包中的 catalina.properites 中进行配置。
3 StandardClassLoader 加载 {catalina.home}/lib 下面的所有 Tomcat 用到的 jar 包。
4 一个 Context 容器,代表了一个 app 应用。Context-->WebappLoader-->WebClassLoader。并且 Thread.contextClassLoader=WebClassLoader。应用程序中的 jsp 文件、class 类、lib/*.jar 包,都是 WebClassLoader 加载的。

Tomcat 加载资源的概况图:



当 Jsp 文件修改的时候,Tomcat 更新步骤:
1 但访问 1.jsp 的时候,1.jsp 的包装类 JspServletWrapper 会去比较 1.jsp 文件最新修改时间和上次的修改时间,以此判断 1.jsp 是否修改过。
2 1.jsp 修改过的话,那么 jspservletWrapper 会清除相关引用,包括 1.jsp 编译后的 servlet 实例和加载这个 servlet 的 JasperLoader 实例。
3 重新创建一个 JasperLoader 实例,重新加载修改过后的 1.jsp,重新生成一个 Servlet 实例。
4 返回修改后的 1.jsp 内容给用户。
图:Jsp 清除引用和资源


当 app 下面的 class 文件修改的时候,Tomcat 更新步骤:
1 Context 容器会有专门线程监控 app 下面的类的修改情况。
2 如果发现有类被修改了。那么调用 Context.reload ()。清楚一系列相关的引用和资源。
3 然后创新创建一个 WebClassLoader 实例,重新加载 app 下面需要的 class。
图:Context 清除引用和资源 

     在一个有一定规模的应用中,如果文件修改多次,重启多次的话,java.lang.OutOfMemoryErrorPermGen space 这个错误的的出现非常频繁。主要就是因为每次重启重新加载大量的 class,超过了 PermGen space 设置的大小。两种情况可能导致 PermGen space 溢出。一、GC (Garbage Collection) 在主程序运行期对 PermGen space 没有进行清理 (GC 的不可控行),二、重启之前 WebClassLoader 加载的 class 在别的地方还存在着引用。这里有篇很好的文章介绍了 class 内存泄露 -here

在 Java 开发领域,热部署一直是一个难以解决的问题,目前的 Java 虚拟机只能实现方法体的修改热部署,对于整个类的结构修改,仍然需要重启虚拟机,对类重新加载才能完成更新操作。对于某些大型的应用来说,每次的重启都需要花费大量的时间成本。虽然 osgi 架构的出现,让模块重启成为可能,但是如果模块之间有调用关系的话,这样的操作依然会让应用出现短暂的功能性休克。本文将探索如何在不破坏 Java 虚拟机现有行为的前提下,实现某个单一类的热部署,让系统无需重启就完成某个类的更新。

类加载的探索

首先谈一下何为热部署(hotswap),热部署是在不重启 Java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为。Java 类是通过 Java 虚拟机加载的,某个类的 class 文件在被 classloader 加载后,会生成对应的 Class 对象,之后就可以创建该类的实例。默认的虚拟机行为只会在启动时加载类,如果后期有一个类需要更新的话,单纯替换编译的 class 文件,Java 虚拟机是不会更新正在运行的 class。如果要实现热部署,最根本的方式是修改虚拟机的源代码,改变 classloader 的加载行为,使虚拟机能监听 class 文件的更新,重新加载 class 文件,这样的行为破坏性很大,为后续的 JVM 升级埋下了一个大坑。

另一种友好的方法是创建自己的 classloader 来加载需要监听的 class,这样就能控制类加载的时机,从而实现热部署。本文将具体探索如何实现这个方案。首先需要了解一下 Java 虚拟机现有的加载机制。目前的加载机制,称为双亲委派,系统在使用一个 classloader 来加载类时,会先询问当前 classloader 的父类是否有能力加载,如果父类无法实现加载操作,才会将任务下放到该 classloader 来加载。这种自上而下的加载方式的好处是,让每个 classloader 执行自己的加载任务,不会重复加载类。但是这种方式却使加载顺序非常难改变,让自定义 classloader 抢先加载需要监听改变的类成为了一个难题。

不过我们可以换一个思路,虽然无法抢先加载该类,但是仍然可以用自定义 classloader 创建一个功能相同的类,让每次实例化的对象都指向这个新的类。当这个类的 class 文件发生改变的时候,再次创建一个更新的类,之后如果系统再次发出实例化请求,创建的对象讲指向这个全新的类。

下面来简单列举一下需要做的工作。

复制
创建自定义的 classloader,加载需要监听改变的类,在 class 文件发生改变的时候,重新加载该类。
改变创建对象的行为,使他们在创建时使用自定义 classloader 加载的 class。

自定义加载器的实现

自定义加载器仍然需要执行类加载的功能。这里却存在一个问题,同一个类加载器无法同时加载两个相同名称的类,由于不论类的结构如何发生变化,生成的类名不会变,而 classloader 只能在虚拟机停止前销毁已经加载的类,这样 classloader 就无法加载更新后的类了。这里有一个小技巧,让每次加载的类都保存成一个带有版本信息的 class,比如加载 Test.class 时,保存在内存中的类是 Test_v1.class,当类发生改变时,重新加载的类名是 Test_v2.class。但是真正执行加载 class 文件创建 class 的 defineClass 方法是一个 native 的方法,修改起来又变得很困难。所以面前还剩一条路,那就是直接修改编译生成的 class 文件。

利用 ASM 修改 class 文件

可以修改字节码的框架有很多,比如 ASM,CGLIB。本文使用的是 ASM。先来介绍一下 class 文件的结构,class 文件包含了以下几类信息:

第一个是类的基本信息,包含了访问权限信息,类名信息,父类信息,接口信息。
第二个是类的变量信息。
第三个是方法的信息。

ASM 会先加载一个 class 文件,然后严格顺序读取类的各项信息,用户可以按照自己的意愿定义增强组件修改这些信息,最后输出成一个新的 class。

首先看一下如何利用 ASM 修改类信息。
清单 1. 利用 ASM 修改字节码

复制
    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); 
    ClassReader cr = null;     
    String enhancedClassName = classSource.getEnhancedName(); 
    try { 
        cr = new ClassReader(new FileInputStream( 
                classSource.getFile())); 
    } catch (IOException e) { 
        e.printStackTrace(); 
        return null; 
    } 
    ClassVisitor cv = new EnhancedModifier(cw, 
            className.replace(".", "/"), 
            enhancedClassName.replace(".", "/")); 
    cr.accept(cv, 0);

ASM 修改字节码文件的流程是一个责任链模式,首先使用一个 ClassReader 读入字节码,然后利用 ClassVisitor 做个性化的修改,最后利用 ClassWriter 输出修改后的字节码。

之前提过,需要将读取的 class 文件的类名做一些修改,加载成一个全新名字的派生类。这里将之分为了 2 个步骤。

第一步,先将原来的类变成接口。
清单 2. 重定义的原始类

复制
public Class<?> redefineClass(String className){ 
    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); 
    ClassReader cr = null; 
    ClassSource cs = classFiles.get(className); 
    if(cs==null){ 
        return null; 
    } 
    try { 
        cr = new ClassReader(new FileInputStream(cs.getFile())); 
    } catch (IOException e) { 
        e.printStackTrace(); 
        return null; 
    } 
    ClassModifier cm = new ClassModifier(cw); 
    cr.accept(cm, 0); 
    byte[] code = cw.toByteArray(); 
    return defineClass(className, code, 0, code.length); 

}

首先 load 原始类的 class 文件,此处定义了一个增强组件 ClassModifier,作用是修改原始类的类型,将它转换成接口。原始类的所有方法逻辑都会被去掉。

第二步,生成的派生类都实现这个接口,即原始类,并且复制原始类中的所有方法逻辑。之后如果该类需要更新,会生成一个新的派生类,也会实现这个接口。这样做的目的是不论如何修改,同一个 class 的派生类都有一个共同的接口,他们之间的转换变得对外不透明。
清单 3. 定义一个派生类

复制
// 在 class 文件发生改变时重新定义这个类
private Class<?> redefineClass(String className, ClassSource classSource){ 
    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); 
    ClassReader cr = null; 
    classSource.update(); 
    String enhancedClassName = classSource.getEnhancedName();       
    try { 
        cr = new ClassReader( 
                new FileInputStream(classSource.getFile())); 
    } catch (IOException e) { 
        e.printStackTrace(); 
        return null; 
    } 
    EnhancedModifier em = new EnhancedModifier(cw, className.replace(".", "/"), 
            enhancedClassName.replace(".", "/")); 
    ExtendModifier exm = new ExtendModifier(em, className.replace(".", "/"), 
            enhancedClassName.replace(".", "/")); 
    cr.accept(exm, 0); 
    byte[] code = cw.toByteArray(); 
    classSource.setByteCopy(code); 
    Class<?> clazz = defineClass(enhancedClassName, code, 0, code.length); 
    classSource.setClassCopy(clazz); 
    return clazz; 

}

再次 load 原始类的 class 文件,此处定义了两个增强组件,一个是 EnhancedModifier,这个增强组件的作用是改变原有的类名。第二个增强组件是 ExtendModifier,这个增强组件的作用是改变原有类的父类,让这个修改后的派生类能够实现同一个原始类(此时原始类已经转成接口了)。

自定义 classloader 还有一个作用是监听会发生改变的 class 文件,classloader 会管理一个定时器,定时依次扫描这些 class 文件是否改变。

改变创建对象的行为

Java 虚拟机常见的创建对象的方法有两种,一种是静态创建,直接 new 一个对象,一种是动态创建,通过反射的方法,创建对象。

由于已经在自定义加载器中更改了原有类的类型,把它从类改成了接口,所以这两种创建方法都无法成立。我们要做的是将实例化原始类的行为变成实例化派生类。

对于第一种方法,需要做的是将静态创建,变为通过 classloader 获取 class,然后动态创建该对象。
清单 4. 替换后的指令集所对应的逻辑

// 原始逻辑
Greeter p = new Greeter();
// 改变后的逻辑
IGreeter p = (IGreeter)MyClassLoader.getInstance().
findClass("com.example.Greeter").newInstance();

这里又需要用到 ASM 来修改 class 文件了。查找到所有 new 对象的语句,替换成通过 classloader 的形式来获取对象的形式。

清单 5. 利用 ASM 修改方法体

复制
@Override 
public void visitTypeInsn(int opcode, String type) { 
    if(opcode==Opcodes.NEW && type.equals(className)){ 
        List<LocalVariableNode> variables = node.localVariables; 
        String compileType = null; 
        for(int i=0;i<variables.size();i++){ 
            LocalVariableNode localVariable = variables.get(i); 
            compileType = formType(localVariable.desc); 
            if(matchType(compileType)&&!valiableIndexUsed[i]){ 
                valiableIndexUsed[i] = true; 
                break; 
            } 
        } 
    mv.visitMethodInsn(Opcodes.INVOKESTATIC, CLASSLOAD_TYPE, 
        "getInstance", "()L"+CLASSLOAD_TYPE+";"); 
    mv.visitLdcInsn(type.replace("/", ".")); 
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, CLASSLOAD_TYPE, 
        "findClass", "(Ljava/lang/String;)Ljava/lang/Class;"); 
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", 
        "newInstance", "()Ljava/lang/Object;"); 
    mv.visitTypeInsn(Opcodes.CHECKCAST, compileType); 
    flag = true; 
    } else { 
        mv.visitTypeInsn(opcode, type); 
    } 
 }

对于第二种创建方法,需要通过修改 Class.forName () 和 ClassLoader.findClass () 的行为,使他们通过自定义加载器加载类。

使用 JavaAgent 拦截默认加载器的行为

之前实现的类加载器已经解决了热部署所需要的功能,可是 JVM 启动时,并不会用自定义的加载器加载 classpath 下的所有 class 文件,取而代之的是通过应用加载器去加载。如果在其之后用自定义加载器重新加载已经加载的 class,有可能会出现 LinkageError 的 exception。所以必须在应用启动之前,重新替换已经加载的 class。如果在 jdk1.4 之前,能使用的方法只有一种,改变 jdk 中 classloader 的加载行为,使它指向自定义加载器的加载行为。好在 jdk5.0 之后,我们有了另一种侵略性更小的办法,这就是 JavaAgent 方法,JavaAgent 可以在 JVM 启动之后,应用启动之前的短暂间隙,提供空间给用户做一些特殊行为。比较常见的应用,是利用 JavaAgent 做面向方面的编程,在方法间加入监控日志等。

JavaAgent 的实现很容易,只要在一个类里面,定义一个 premain 的方法。
清单 6. 一个简单的 JavaAgent

复制
 public class ReloadAgent { 
    public static void premain(String agentArgs, Instrumentation inst){ 
        GeneralTransformer trans = new GeneralTransformer(); 
        inst.addTransformer(trans); 
    } 
 }

然后编写一个 manifest 文件,将 Premain-Class 属性设置成定义一个拥有 premain 方法的类名即可。

生成一个包含这个 manifest 文件的 jar 包。

复制
 manifest-Version: 1.0 
 Premain-Class: com.example.ReloadAgent 
 Can-Redefine-Classes: true

最后需要在执行应用的参数中增加 -javaagent 参数,加入这个 jar。同时可以为 Javaagent 增加参数,下图中的参数是测试代码中 test project 的绝对路径。这样在执行应用的之前,会优先执行 premain 方法中的逻辑,并且预解析需要加载的 class。

这里利用 JavaAgent 替换原始字节码,阻止原始字节码被 Java 虚拟机加载。只需要实现 一个 ClassFileTransformer 的接口,利用这个实现类完成 class 替换的功能。
清单 7. 替换 class

复制
@Override 
public byte [] transform(ClassLoader paramClassLoader, String paramString, 
     Class<?> paramClass, ProtectionDomain paramProtectionDomain, 
     byte [] paramArrayOfByte) throws IllegalClassFormatException { 
    String className = paramString.replace("/", "."); 
    if(className.equals("com.example.Test")){ 
        MyClassLoader cl = MyClassLoader.getInstance(); 
        cl.defineReference(className, "com.example.Greeter"); 
        return cl.getByteCode(className); 
    }else if(className.equals("com.example.Greeter")){ 
        MyClassLoader cl = MyClassLoader.getInstance(); 
        cl.redefineClass(className); 
        return cl.getByteCode(className); 
    } 
    return null; 
 }

至此,所有的工作大功告成,欣赏一下 hotswap 的结果吧。

基于 Java 类加载原理实验类动态替换

   一个小实验,基于对 Java 类加载模型的理解,实现一个 Java 类动态替换的效果,仅供学习理解 Java 类加载机制。
关键:
(1)JVM 通过类的加载器和类全名组合识别一个类。
(2)类加载器层次结构及委托加载模型
(3)自定义类加载器

示例:
(1)定义一个服务接口及其实现类:AService 和 SimpleA,提供简单的 add 服务。
AService
Java 代码  收藏代码
  1. package org.demo;  
  2.   
  3. public interface AService {  
  4.   
  5.     public int add(int a, int b);  
  6. }  


SimpleA
Java 代码  收藏代码
  1. package org.demo;  
  2.   
  3. public class SimpleA implements AService {  
  4.   
  5.     @Override  
  6.     public int add(int a, int b) {  
  7.         int result = 0;  
  8.         result = a + b;  
  9.         return result;  
  10.     }  
  11.   
  12. }  


(2)自定义类加载器 HotreplaceLoader,加载示例中需要动态替换的类
Java 代码  收藏代码
  1. package org.demo;  
  2.   
  3. import java.io.ByteArrayOutputStream;  
  4. import java.io.File;  
  5. import java.io.FileInputStream;  
  6.   
  7. public class HotreplaceLoader extends ClassLoader {  
  8.   
  9.     public HotreplaceLoader(){  
  10.         super(null);  
  11.     }  
  12.       
  13.     @Override  
  14.     protected Class<?> findClass(String name) throws ClassNotFoundException {  
  15.         byte[] data = loadClassData(name);  
  16.         return this.defineClass(name, data, 0, data.length);  
  17.     }  
  18.       
  19.     public byte[] loadClassData(String name) {  
  20.         try {  
  21.             name = name.replace(".""//");  
  22.             FileInputStream is = new FileInputStream(new File(  
  23.                     "E:\\loadclass\\src\\" + name + ".class"));  
  24.             ByteArrayOutputStream baos = new ByteArrayOutputStream();  
  25.             int b = 0;  
  26.             while ((b = is.read()) != -1) {  
  27.                 baos.write(b);  
  28.             }  
  29.             return baos.toByteArray();  
  30.         } catch (Exception e) {  
  31.             e.printStackTrace();  
  32.         }  
  33.         return null;  
  34.     }  
  35.   
  36.     @Override  
  37.     public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {  
  38.         Class cls = null;  
  39.         cls = findLoadedClass(name);  
  40.         if(!name.equals("org.demo.SimpleA") && cls == null){  
  41.             cls = getSystemClassLoader().loadClass(name);  
  42.         }else{  
  43.             try{  
  44.                 cls = findClass("org.demo.SimpleA");  
  45.             }catch(Exception e){  
  46.                 e.printStackTrace();  
  47.             }  
  48.         }  
  49.         if (cls == null)  
  50.             throw new ClassNotFoundException(name);  
  51.         if (resolve)  
  52.             resolveClass(cls);  
  53.         return cls;  
  54.     }  
  55.       
  56. }  


(3)主进程 Worker,无限循环加载目标类,并调用其服务,每次调用后休息
Java 代码  收藏代码
  1. package org.demo;  
  2.   
  3. import java.lang.reflect.Method;  
  4.   
  5. public class Worker {  
  6.   
  7.     public static void main(String[] args) throws Exception{  
  8.         for(;;){  
  9.             HotreplaceLoader loader = new HotreplaceLoader();  
  10.             Class cls = loader.loadClass("org.demo.SimpleA");  
  11.             //Object aService = cls.newInstance();  
  12.             //Method m = aService.getClass().getMethod("add", new Class[]{int.class, int.class});  
  13.             //Object count = m.invoke(aService, new Object[]{1, 1});  
  14.             AService as = (AService)cls.newInstance();  
  15.             int count = as.add(11);  
  16.             System.out.println(String.valueOf(count));  
  17.             try{  
  18.                 Thread.sleep(1000);  
  19.             }catch(InterruptedException e){  
  20.                 e.printStackTrace();  
  21.             }  
  22.         }  
  23.     }  
  24.   
  25. }  


(4)验证
执行主进程,控制台不断打出 “2”。
修改 SimpleA 源码为:
Java 代码  收藏代码
  1. public class SimpleA implements AService {  
  2.   
  3.     @Override  
  4.     public int add(int a, int b) {  
  5.         int result = 0;  
  6.         result = (a + b)*2;  
  7.         return result;  
  8.     }  
  9.   
  10. }  

然后将编译好的字节码 SimleA.class, 拷贝到设定的加载目录 E:\loadclass\src\ 替换原来的字节码文件。
控制台变化,不断打出 “4”。

java - 如何使用 URLClassLoader 加载 *.class 文件? - 堆栈溢出 --- java - How to use URLClassLoader to load a *.class file? - Stack Overflow

您必须向 URLClassLoader 提供包含 .class 文件的目录或 jar 文件:

classUrl = new URL("file:///home/kent/eclipsews/SmallExample/bin/");

是的,您可以加载任意数量的类

您必须通过提供完全限定的类名(即类名及其包路径)来加载该类,

Class c = ucl.loadClass("com.mypackage.IndependentClass");

我遇到了同样的问题,但我发现这是一个解决方案:

System.getProperty("java.class.path")

这将为您提供 jar 和类路径。从这里您可以管理您的流程。

 

 

posted @ 2024-06-26 16:32  CharyGao  阅读(13)  评论(0编辑  收藏  举报