资讯专栏INFORMATION COLUMN

自定义类加载器-从.class和.jar中读取

Jackwoo / 3375人阅读

摘要:在没有指定自定义类加载器的情况下,这就是程序的默认加载器。自定义类加载器双亲委派模型避免由于字节码被多次加载。首先自定义类加载器,最重要的就是先继承这个类。

一. 类加载器

JVM中的类加载器:在jvm中,存在两种类加载器,
a) Boostrap ClassLoader:这个是由c++实现的,所以在方法区并没有Class对象的实例存在。用于加载JAVA_HOME/bin目录下的jar包

b) 其他类加载器:由java实现,可以在方法区找到其Class对象。这里又细分为几个加载器

扩展类加载器(Extension ClassLoader):它负责用于加载JAVA_HOME/lib/ext目录中的,或者被java.ext.dirs系统变量指定所指定的路径中所有类库,开发者可以直接使用扩展类加载器。java.ext.dirs系统变量所指定的路径的可以通过程序来查看。System.getProperty("java.ext.dirs")

应用程序类加载器(Application ClassLoader):负责加载用户类路径上指定的类库。开发者可以直接使用这个类加载器。ps:在没有指定自定义类加载器的情况下,这就是程序的默认加载器。

自定义类加载器(User ClassLoader):

双亲委派模型:避免由于Class字节码被多次加载。底层类加载器在收到一个类加载的请求的时候,都先把请求转发给其父加载器(并不是一个继承的关系),父类查找不到才会让子类去加载。如果强制只有双亲委派模型,那么,web服务器的隔离是无法实现的。

  由于最近想做一个类似tomcat一样的简易版web服务器来加深理解http请求的处理过程。我们都清楚,每个web应用在tomcat中都可以使用自己版本的jar。除了少量的包,如Servlet-api.jar,还有一些java原生的包之外,tomcat是会为每个不同的应用加载不同的jar包或者class,且彼此之间不会相互影响。这一步是通过自定义类加载器来实现的,在虚拟机层面,判断两个Class是否相等的前提是他们是同一个类加载器加载的,否则就没有意义了。这篇文章简单的实现一个自定义的加载过程,PS:这个例子并没有破坏双亲委派模型,因为例子中依然会查找父类,如果找不到再使用子类加载。接下来笔者会再更新破坏双亲委派模型的博客,这里挖个坑。
  首先自定义类加载器,最重要的就是先继承ClassLoader这个类。加载器的加载流程是,给出一个Class文件的全限定名,然后调用loadClass方法,这个方法每部会现在自己已经加载的类中查找,如果找到就返回。找不到则向父类查找,如果父类都找不到这才开始自己加载,调用findClass方法。所以我们只要覆盖findClass方法就可以实现自己定义的加载了。顺带一提,ClassLoader中有一个方法叫defineClass(String, byte[], int, int);这个方法通过传进去一个Class文件的字节数组,就可以方法区生成一个Class对象。所以要实现findClass的目标就很明确了,只要将Class文件读取进来,然后生成byte数组,调用defineClass方法就可以了。

@Override
    protected Class findClass(String name){
        try {
            byte[] result = getClassFromFileOrMap(name);
            if(result == null){
                throw new FileNotFoundException();
            }else{
                return defineClass(name, result, 0, result.length);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

  那么如何找到Class文件呢?手动生成/网络下载我们就暂时不谈论。这里只说两种最常见的,一是直接.class文件中查找,二是从jar包中加载。
从class文件中加载非常简单。只要找到相应的文件,就可以通过字节流读取进来。代码如下:

input = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
int bufferSize = 4096; 
byte[] buffer = new byte[bufferSize]; 
int bytesNumRead = 0; 
while ((bytesNumRead = input.read(buffer)) != -1) { 
    baos.write(buffer, 0, bytesNumRead); 
}
return baos.toByteArray();

  从jar读取则相对麻烦一点,java给我们提供了一个专门用来读取jar包文件的类,抽象成一个JarFile的对象。通过调用这个对象的getInputStream方法,也是可以获取文件的输入流,从而读取字节数组。笔者做了一点相应的缓存,如果每次查找文件都要先读取jar文件,再遍历查找class文件是非常耗时的操作。于是,笔者选择再加载之前,把所有的jar包中的所有class读取到内存中,保存在一个map对象中。建立一个全限定名和字节数组的映射。这样在加载阶段,就能省下很多的时间了。全部的代码如下

package com.chasel.cloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * 自定义的类加载器【子类优先】
 * @author hujiancai
 * @description 
 * @data 2017年3月11日
 * @version v_0.1
 */
public class MyWebAppLoader extends ClassLoader{
    /**
     * lib:表示加载的文件在jar包中
     * 类似tomcat就是{PROJECT}/WEB-INF/lib/
     */
    private String lib;
    /**
     * classes:表示加载的文件是单纯的class文件
     * 类似tomcat就是{PROJECT}/WEB-INF/classes/
     */
    private String classes;
    /**
     * 采取将所有的jar包中的class读取到内存中
     * 然后如果需要读取的时候,再从map中查找
     */
    private Map map;
    
    /**
     * 只需要指定项目路径就好
     * 默认jar加载路径是目录下{PROJECT}/WEB-INF/lib/
     * 默认class加载路径是目录下{PROJECT}/WEB-INF/classes/
     * @param webPath
     * @throws MalformedURLException 
     * @throws SecurityException 
     * @throws NoSuchMethodException 
     */
    public MyWebAppLoader(String webPath) throws NoSuchMethodException, SecurityException, MalformedURLException{
        lib = webPath + "WEB-INF/lib/";
        classes = webPath + "WEB-INF/classes/";
        map = new HashMap(64);
        
        preReadJarFile();
    }

    /**
     * 按照父类的机制,如果在父类中没有找到的类
     * 才会调用这个findClass来加载
     * 这样只会加载放在自己目录下的文件
     * 而系统自带需要的class并不是由这个加载
     */
    @Override
    protected Class findClass(String name){
        try {
            byte[] result = getClassFromFileOrMap(name);
            if(result == null){
                throw new FileNotFoundException();
            }else{
                return defineClass(name, result, 0, result.length);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    
    /**
     * 从指定的classes文件夹下找到文件
     * @param name
     * @return
     */
    private byte[] getClassFromFileOrMap(String name){
        String classPath = classes + name.replace(".", File.separatorChar) + ".class";
        File file = new File(classPath);
        if(file.exists()){
            InputStream input = null;
            try {
                input = new FileInputStream(file);
                ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
                int bufferSize = 4096; 
                byte[] buffer = new byte[bufferSize]; 
                int bytesNumRead = 0; 
                while ((bytesNumRead = input.read(buffer)) != -1) { 
                    baos.write(buffer, 0, bytesNumRead); 
                }
                return baos.toByteArray();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally{
                if(input != null){
                    try {
                        input.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            
        }else{
            if(map.containsKey(name)) {
                //去除map中的引用,避免GC无法回收无用的class文件
                return map.remove(name);
            }
        }
        return null;
    }
    
    /**
     * 预读lib下面的包
     */
    private void preReadJarFile(){
        List list = scanDir();
        for(File f : list){
            JarFile jar;
            try {
                jar = new JarFile(f);
                readJAR(jar);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    /**
     * 读取一个jar包内的class文件,并存在当前加载器的map中
     * @param jar
     * @throws IOException
     */
    private void readJAR(JarFile jar) throws IOException{
        Enumeration en = jar.entries();
        while (en.hasMoreElements()){
            JarEntry je = en.nextElement();
            String name = je.getName();
            if (name.endsWith(".class")){
                String clss = name.replace(".class", "").replaceAll("/", ".");
                if(this.findLoadedClass(clss) != null) continue;
                
                InputStream input = jar.getInputStream(je);
                ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
                int bufferSize = 4096; 
                byte[] buffer = new byte[bufferSize]; 
                int bytesNumRead = 0; 
                while ((bytesNumRead = input.read(buffer)) != -1) { 
                    baos.write(buffer, 0, bytesNumRead); 
                }
                byte[] cc = baos.toByteArray();
                input.close();
                map.put(clss, cc);//暂时保存下来
            }
        }
    }
    
    /**
     * 扫描lib下面的所有jar包
     * @return
     */
    private List scanDir() {
        List list = new ArrayList();
        File[] files = new File(lib).listFiles();
        for (File f : files) {
            if (f.isFile() && f.getName().endsWith(".jar"))
                list.add(f);
        }
        return list;
    }
    
    /**
     * 添加一个jar包到加载器中去。
     * @param jarPath
     * @throws IOException 
     */
    public void addJar(String jarPath) throws IOException{
        File file = new File(jarPath);
        if(file.exists()){
            JarFile jar = new JarFile(file);
            readJAR(jar);
        }
    }
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/66798.html

相关文章

  • JVM实战---加载的过程

    任何程序都需要加载到内存才能与CPU进行交流 同理, 字节码.class文件同样需要加载到内存中,才可以实例化类 ClassLoader的使命就是提前加载.class 类文件到内存中 在加载类时,使用的是Parents Delegation Model(溯源委派加载模型) Java的类加载器是一个运行时核心基础设施模块,主要是在启动之初进行类的加载、链接、初始化 showImg(https://s...

    bladefury 评论0 收藏0
  • 详细深入分析 Java ClassLoader 工作机制

    摘要:作用负责将加载到中审查每个类由谁加载父优先的等级加载机制将字节码重新解析成统一要求的对象格式类结构分析为了更好的理解类的加载机制,我们来深入研究一下和他的方法。就算两个是同一份字节码,如果被两个不同的实例所加载,也会认为它们是两个不同。 申明:本文首发于 详细深入分析 ClassLoader 工作机制 ,如有转载,注明原出处即可,谢谢配合。 什么是 ClassLoader ? 大家...

    mdluo 评论0 收藏0
  • JAVA加载机制全解析

    摘要:当程序使用某个类时,如果该类还没被初始化,加载到内存中,则系统会通过加载连接初始化三个过程来对该类进行初始化。一旦一个类被加载到中之后,就不会再次载入了。它既可以从本地文件系统获取二进制文件来加载类,也可以远程主机获取二进制文件来加载类。 当程序使用某个类时,如果该类还没被初始化,加载到内存中,则系统会通过加载、连接、初始化三个过程来对该类进行初始化。该过程就被称为类的初始化 类加载 ...

    tomener 评论0 收藏0
  • 纪念我曾经的 JAVA 姿势

    摘要:,关闭不当编译器警告信息。创建固定大小的线程池。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统或者说能够创建的最大线程大小。此线程池支持定时以及周期性执行任务的需求。 目前在搞 Node.js,曾经的 JAVA 知识忘了好多,为此整理了下,感叹下工业语言还是有相当的优势的。 流 Java所有的流类位于java.io包中,都分别继承字以下四种抽象流类型。 Type 字节...

    The question 评论0 收藏0
  • 加载机制,双亲委派模型,搞定大厂高频面试题

    摘要:验证验证是连接阶段的第一步,这一阶段的目的是为了确保文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。字节码验证通过数据流和控制流分析,确定程序语义是合法的符合逻辑的。 看过这篇文章,大厂面试你「双亲委派模型」,硬气的说一句,你怕啥? 读该文章姿势 打开手头的 IDE,按照文章内容及思路进行代码跟踪与思考 手头没有 IDE,先收藏,回头看 (万一哪次面试问...

    Object 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<