如何实现自己的ClassLoader

时间:2022-07-25
本文章向大家介绍如何实现自己的ClassLoader,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

ClassLoader能够完成的事情无非有以下几种情况:

  • 在自定义路径下查找自定义的class类文件,也许我们需要的class文件并不总是已经设置好的classpath下,那么我摸嗯必须想办法来找到这个类,在这种情况下,我们需要自己实现一个ClassLoader
  • 对我们自己的要加载的类做特殊处理,如保证通过网络传输的类的安全性,可以将类经过加密后再传输,在加载到JVM之前需要对类的字节码再解密,,这个过程就可以在自定义的ClassLoader中实现。
  • 可以定义类的实现机制,如果我们可以检查已经加载的calss文件是否修改,如果修改了,可以重新加载这个类,从而实现类的热部署。

加载自定义路径下的class文件

我们自己实现一个ClassLoader,并指定这个ClassLoader的加载路径可以通过如下方式来实现;

import java.io.*;

public class PathClassLoader extends ClassLoader{
    private String classPath;
    private String packageName;

    public PathClassLoader(String classPath,String packageName) {
        this.classPath = classPath;
        this.packageName = packageName;
    }

    protected  Class<?> findClass(String name) throws ClassNotFoundException {
        if (packageName.startsWith(name)){
            byte[] classData = getData(name);
            if (classData!=null){
                return  defineClass(name,classData,0,classData.length);
            }
            else {
                throw new ClassNotFoundException();
            }
        }
        else {
            return super.loadClass(name);
        }
    }

    private byte[] getData(String className) {
        String path = classPath+ File.separatorChar+className.replace('.',File.separatorChar)+".class";
        try {
            InputStream is = new FileInputStream(path);
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int num =0;
            while ((num=is.read(buffer))!=-1){
                stream.write(buffer,0,num);
            }
            return stream.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

在上面这段代码中,到classpath目录下去加载指定包名的class文件,如果不是非设置好的class path,仍然使用父类加载器去加载。 还有一种方式是继承URLClassLoader类,然后设置自定义路径的URL来加载URL下的类,这种方式更佳,如下:

import java.net.URL;
import java.net.URLClassLoader;

public class URLPathClassLoader extends URLClassLoader{
    private String packageName = "your.name.classPackage";

    public URLPathClassLoader(URL[] classPath,ClassLoader parent) {
        super(classPath,parent);
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> aClass = findLoadedClass(name);
        if (aClass!=null){
            return aClass;
        }
        if (packageName.startsWith(name)){
            return super.loadClass(name);
        }else {
            return findClass(name);
        }
    }

}

我们将指定的目录转换为URL路i纪念馆,然后作为参数创建URLPathClassLoader 对象,那么这个ClassLoader在加载时就在UTL指定的目录下查找指定的类文件。

加载自定义格式的calss文件

假设我们通过网络从远处主机上下载一个class文件的字节码,但是为了安全性,在传输之前对这个字节码进行了简单的加密处理,然后再通过网络传世。当客户端接收到这个类的字节码后需要经过解密才能还原成原始的格式,然后再通过ClassLoader的defineClass()方法创建这个java.lang.class的实例,最后完成类的加载工作,如下:

import java.io.*;

public class NetClassLoader extends ClassLoader{
    private String classPath;
    private String packageName = "net.you.classloader";

    public NetClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> aClass = findLoadedClass(name);
        if (aClass!=null){
            return aClass;
        }
        if (packageName.startsWith(name)){
                    byte[] classData = getData(name);
                   if (classData!=null){
                       return  defineClass(name,classData,0,classData.length);
                   }
                   else {
                       throw new ClassNotFoundException();
                   } 
        }else {
               return super.loadClass(name);
           }
        }

        private byte[] getData(String className) {
            String path = classPath+ File.separatorChar+className.replace('.',File.separatorChar)+".class";
            try {
                InputStream is = new FileInputStream(path);
                ByteArrayOutputStream stream = new ByteArrayOutputStream();
                byte[] buffer = new byte[2048];
                int num =0;
                while ((num=is.read(buffer))!=-1){
                    stream.write(buffer,0,num);
                }
                return stream.toByteArray();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
        protected byte[] deCode(byte[] src){
            byte[] decode = null;
            //对src字节码进行解码处理
            return decode;
        }
    }

在方法decode()中 ,可以对i网络传输过来的字节码进行某种解码处理, 然后返回正确的class字节码,调用defineClass()来创建java.lang.class的实例

实现类的热部署

我们知道,JVM再加载类之前会 检查请求的类是否已经被加载过来,也就是要调用findLoadedClass()方法查看是否能够返回实例。如果类已经加载过来,再调用loadClass()将会导致类冲突。但是JVM表示一个类是否是同一个类会有两个条件。一是看这个类的完整类名是否一样,这个类名包括类所在的包名。二是看加载这个类的ClassLoader是否是通过一个,这里所说的同一个是指ClassLoader的实例是否是同一个实例。即便是同一个ClassLoader类的两个实例,加载同一个类也会不一样。所以要实现类的热部署可以创建不同的ClassLoader的实例对象,然后通过这个不同的实例对象来加载同名的类,如下:

import java.io.*;

public class ClassReloader extends ClassLoader{
    private  String classPath;


    public ClassReloader(String classPath) {
        this.classPath = classPath;
    }
    protected  Class<?> findClass(String name) throws ClassNotFoundException {
            byte[] classData = getData(name);
            if (classData!=null){
                return  defineClass(name,classData,0,classData.length);
            }
            else {
                throw new ClassNotFoundException();
            }
    }

    private byte[] getData(String className) {
        String path = classPath+ File.separatorChar+className.replace('.',File.separatorChar)+".class";
        try {
            InputStream is = new FileInputStream(path);
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int num =0;
            while ((num=is.read(buffer))!=-1){
                stream.write(buffer,0,num);
            }
            return stream.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args)  {
       try {
           String path = "E:\workspace\jwt_demo\target\classes";
           ClassReloader reloader = new ClassReloader(path);
           String classname = "com.hxuhao.model.User";
           Class r = reloader.findClass(classname);
           System.out.println(r.newInstance());
           ClassReloader reloader1 = new ClassReloader(path);
           Class r1 = reloader1.findClass(classname);
           System.out.println(r1.newInstance());
       }catch (Exception e){
           e.printStackTrace();
       }

    }
}

运行结果

com.hxuhao.model.User@6d6f6e28
com.hxuhao.model.User@330bedb4

运行上面的代码打印出来的是两个不同 的类实例对象,如果不是创建了两个不同的ClassReloader对象,如将上面main方法改成下面的代码:

 public static void main(String[] args)  {
       try {
           String path = "E:\workspace\jwt_demo\target\classes";
           ClassReloader reloader = new ClassReloader(path);
           String classname = "com.hxuhao.model.User";
           Class r = reloader.findClass(classname);
           System.out.println(r.newInstance());
           Class r1 = reloader.findClass(classname);
           System.out.println(r1.newInstance());
       }catch (Exception e){
           e.printStackTrace();
       }

    }

运行后,报错: 重复加载一个类就会抛出java.lang.LinkageError

com.hxuhao.model.User@6d6f6e28
Exception in thread "main" java.lang.LinkageError: loader (instance of  ClassReloader): attempted  duplicate class definition for name: "com/hxuhao/model/User"
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
    at ClassReloader.findClass(ClassReloader.java:13)
    at ClassReloader.main(ClassReloader.java:46)

使用不同的Classloader实例加载同一个类,会不会导致JVM的PermGen区无限增大?答案是否定的,因为我们的classloader对象也会和其他对象一样,当没有对象再引用它以后,也会被JVM回收。但是需要注意的一点是,被这个Classloader加载的类的字节码会保存在JVM的PermGen区,这个数据一般只是在执行Full GC时才会被回收的,所以如果在你的应用中都是大量的动态类加载,FUll GC 又不是太频繁,也要主要permGen区的大小,防止内存溢出。

Java应不应该动态加载类

我想大家都知道用JAVA有一个痛楚,就是修改一个类,必须重启一遍,很费时。于是就想能不能来个动态类的加载而不需要重启JVM,如果你了解JVM的工作机制,就应该放弃这个念头。

Java的优势正式基于共享对象的机制,达到信息的高度共享,也就是通过保存并持有对象的状态而省去类信息的重复创建和回收。我们知道对象一旦被创建,这个对象就可以被人持有和利用。 假如,我只说说,假如我们能够动态加载一个对象进入JVM,但是如何做到JVM中对象的平滑过渡?几乎不可能!虽然在JVM中对象只有一份,在理论上可以直接替换这个对象,然后更新Java栈中所有对原对象的引用关系。看起来好像对象可以被替换了, 但是这仍然不可行,因为它违反了JVM的设计原则,对象的引用关系只有对象的创建者持有和使用,JVM不可以干预对象的引用关系,因为JVM并不知道对象时怎么被使用的,这就涉及JVM并不知道对象的运行时类型而只知道编译时类型。 假如一个对象的属性结构被修改,但是在运行时其他对象可能仍然引用该属性。 虽然完全的无障碍的替换时不现实的。但是如果你非要那么做,也还是有一些“旁门左道”的。前面的分析造成不能动态提供类对象的关键是,对象的状态被保存了,并且被其他对象引用了,一个简单的解决方法就是不保存对象的状态,对象被创建使用后就被释放掉,下次修改后,对象也就是新的了 这种方式是不是很好呢?这就是JSP,它难道不是可以动态加载吗?也许你已经想到了,所有其他解释性语言都是如此。