类加载器是怎么加载包下的类呢?

  《深入理解Java虚拟机》中有提到,只有在同一个类加载器加载出来的类,才具有类之间比较的价值。所以本文用一个简单的累加器例子来了解类加载器是怎么加载类。

    在此,分为三个部分。

  A:获取类加载器;

  B:加载类;

  C 获取指定包下的所有类。

public class ClassUtil {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(ClassUtil.class);
    
    /**
     * 获取类加载器
     * @return
     */
    public static ClassLoader getClassLoader() {
        //A
    }
    
    /**
     * 加载类
     * @param className
     * @param isInitialized
     * @return
     */
    public static Class<?> loadClass(String className, boolean isInitialized){
        //B
    }
    
    /**
     * 获取指定包名下的所有类
     * @param packageName
     * @return
     */
    public static Set<Class<?>> getClassSet(String packageName){
        //C
    }
}

  先有鸡才有蛋,所以在加载类之前,得先有类加载器。类加载器可以用JDK自带的,也可以自定义一个ClassLoader,此处使用当前线程中的ClassLoader,即A部分:

public static ClassLoader getClassLoader() {
    return Thread.currentThread().getContextClassLoader();
}

  其中,Thread.currentThread()可以获取到当前线程并对其进行操作,如suspend()。

  接着我们可以加载类了,而加载类我们此处通过Class.forName()反射得到。

public static Class<?> loadClass(String className, boolean isInitialized){
        Class<?> cls;
        try {
            cls = Class.forName(className, isInitialized, getClassLoader());
            ClassLoader.getSystemClassLoader().loadClass(className);
        }catch(ClassNotFoundException e) {
            LOGGER.error("load class failure", e);
            throw new RuntimeException(e);
        }
        return cls;
    }

  此处可能会有小伙伴问,Class.forName()和ClassLoader.loadClass()加载类有什么区别呢?

  下面是本人的浅见。(此部分,须了解过Java虚拟机哦,不感兴趣的小伙伴可以略过~)

  分为两部分:

  1. 我们先来了解一下Class.forName(String name)和ClassLoader.loadClass(String name)的区别,可以参考此文:https://www.cnblogs.com/zabulon/p/5826610.html。简单来说就是,这两个方法在类加载时期的初始化、链接的操作不同,导致它们在静态资源加载方面产生不同的结果。那么如果我们将这一层的不同进行透明化呢?其实还是不同,因为他们加载的机制(或者说是算法)不一样。

  2. Class.forName()和ClassLoader.loadClass()机制不同在哪儿呢?我们先看Class.forName()。

    /**
     * Returns the {@code Class} object associated with the class or
     * interface with the given string name, using the given class loader.
     * Given the fully qualified name for a class or interface (in the same
     * format returned by {@code getName}) this method attempts to
     * locate, load, and link the class or interface.  The specified class
     * loader is used to load the class or interface.  If the parameter
     * {@code loader} is null, the class is loaded through the bootstrap
     * class loader.  The class is initialized only if the
     * {@code initialize} parameter is {@code true} and if it has
     * not been initialized earlier.*/
    @CallerSensitive
    public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader)
        throws ClassNotFoundException
    {
        ....return forName0(name, initialize, loader, caller);
    }

    /** Called after security check for system loader access checks have been made. */
    private static native Class<?> forName0(String name, boolean initialize,
                                            ClassLoader loader,
                                            Class<?> caller)
        throws ClassNotFoundException;

  此处截取了一部分关于Class.forName()的注释,加粗部分的意思是“如果传入的ClassLoader为null的话,就直接使用Bootstrap类加载器”。 那么ClassLoader.loadClass()是怎么样的呢? ClassLoader.loadClass()最后也可能会用到Bootstrap类加载器,但是还会经过两步才会到使用启动类加载器,第一步是先找是否已经加载过此类,第二步是在双亲委派模型的基础上查找父类,使用父类加载器加载类。

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                ......
        }
    }

  在ClassLoader.loadClass()中,会先去查找是否这个类已经被加载过了,findLoadedClass()方法是本地方法。如果没找到,再加载。假设大家知道类加载器的双亲委派模型即如果一个加载器接收到类加载的请求,它会先将这个请求交给父类加载器去完成,各层次的类加载器都是这么处理请求的,只有当父加载器反馈自己无法完成这个加载请求的时候,子加载器才会尝试自己去加载。  所以此处也是先去查找此类的父类加载器对这个类进行加载。

/**
* Returns a class loaded by the bootstrap class loader;
* or return null if not found.
*/
private Class<?> findBootstrapClassOrNull(String name)
{
  if (!checkName(name)) 
    return null;
  return findBootstrapClass(name);
}

    如果该加载器没有父加载器,才会将请求直接交给Bootstrap加载器。

    综上,Class,forName()和ClassLoader.loadClass()的不同之处有二,一是对于类加载过程中的把控部分不同;二是加载类选择类加载器的方式不同。

    续上加载类的部分,接下来是怎么获取指定包名下的所有类。

/**
     * 获取指定包名下的所有类
     * @param packageName
     * @return
     */
    public static Set<Class<?>> getClassSet(String packageName){
        Set<Class<?>> classSet = new HashSet<Class<?>>();
        try {
            //将包名转化为文件路径
            Enumeration<URL> urls = getClassLoader().getResources(packageName.replace(".", "/"));
            while(urls.hasMoreElements()) {
                URL url = urls.nextElement();
                if(url != null) {
                    //获取文件的类型
                    String protocol = url.getProtocol();
                    //如果是"file"类型,就拿到这个文件里面的class文件
                    if(protocol.equals("file")) {
                        String packagePath = url.getPath().replaceAll("%20", " ");
                        addClass(classSet, packagePath, packageName);
                        //如果是'jar'类型的,先读取这个包里面的文件,如果jar中有'class'文件,则读取出来
                    }else if(protocol.equals("jar")) {
                        JarURLConnection jarURLConnection = (JarURLConnection)url.openConnection();
                        if(jarURLConnection != null) {
                            JarFile jarFile = jarURLConnection.getJarFile();
                            if(jarFile != null) {
                                Enumeration<JarEntry> jarEntries = jarFile.entries();
                                while(jarEntries.hasMoreElements()) {
                                    JarEntry jarEntry = jarEntries.nextElement();
                                    String jarEntryName = jarEntry.getName();
                                    if(jarEntryName.endsWith(".class")) {
                                        String className = jarEntryName.substring(0, jarEntryName.lastIndexOf(".")).replaceAll("/", ".");
                                        doAddClass(classSet, className);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }catch(Exception e) {
            LOGGER.error("get class set failure", e);
            throw new RuntimeException(e);
        }
        
        return classSet;
    }

    private static void doAddClass(Set<Class<?>> classSet, String className) {
        Class<?> cls = loadClass(className, false);
        classSet.add(cls);
        
    }

    private static void addClass(Set<Class<?>> classSet, String packagePath, String packageName) {
        File[] files = new File(packagePath).listFiles(new FileFilter() {

            @Override
            public boolean accept(File file) {
                return (file.isFile() && file.getName().endsWith(".class")) || file.isDirectory();
            }
            
        });
        
        for(File file: files) {
            String fileName = file.getName();
            if(file.isFile()) {
                String className = fileName.substring(0, fileName.lastIndexOf("."));
                if(StringUtil.isNotEmpty(packageName)) {
                    className = packageName + "." + className; 
                }
                doAddClass(classSet, className);
            }else {
                String subPackagePath = fileName;
                if(StringUtil.isNotEmpty(packagePath)) {
                    subPackagePath = packagePath + "/" + subPackagePath;
                }
                String subPackageName = fileName;
                if(StringUtil.isNotEmpty(packageName)) {
                    subPackageName = packageName + "." +subPackageName;
                }
            }
        }
    }

  此部分,概括起来就是,通过反射的方式,将这个包下的file类型文件和jar类型文件分来加载,如果是jar类型的文件,则将jar中的class文件加载出来。

  总结一下哈,本文通过一个简单的类加载器工具类,讲述了加载包中的类的过程:1. 先将包中的文件分为两种,一种是file类型的文件,这种直接加载,另一种是jar类型的文件,这种还需要将文件包打开,将其中的,class文件通过加载类方法将类加载出来。而且本文还稍稍提及了一下Class.forName()和ClassLoader.loadClass()这两种加载类的方法的不同之处。

 

posted @ 2019-11-20 15:46  NYfor2018  阅读(617)  评论(0编辑  收藏  举报