URLClassloader的实现加载类原理

2,791 阅读4分钟

首先来看下URLClassloader的example

image.png 从例子中可以看到只要创建URLClassloader并传入URL的集合,然后直接调用loadCLass传入类资源全限定名称, 就可以实现对类的URL中资源的class字节码加载到JVM成为Java.lang.Class对象。

URLClassloader的类继承关系

URLClassloader继承自SecureClassloader以及Classloader。

image.png

URLClassloader的重要字段就是ucp,存放的是类和资源的路径

/* The search path for classes and resources */
private final URLClassPath ucp;

URLClassLoader的构造函数传资源的加载路径,

  1. 初始化父类的构造函数。
  2. 创建URLClassPath对象,并传入资源的urls以及访问上下文AccessContext。
public URLClassLoader(URL[] urls) {
    super();
    this.acc = AccessController.getContext();
    this.ucp = new URLClassPath(urls, acc);
}

URLClassloader重写父类的findClass

由前面的文章可以知道URLClassloader继承Classloader只是重写了父类的findClass方法的实现径,这里实现AccessController#doPrivileged传入匿名内部类,是使用模板类在执行run函数的前后检查是否由访问权限,这里就不多说,下面直接看run方法的逻辑

  1. 将传入类的路径名称字符串中所有的'.'替换成'/',结尾加'.class'。
  2. 通过ucp(构造函数中创建的URLClassPath对象的变量)的getResource方法去查询
  3. 如果查询Resouce不为空,则调用defineClass进行类资源的加载并转换成Class对象.
protected Class<?> findClass(final String name)throws ClassNotFoundException{
    final Class<?> result;
    try {
        result = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class<?>>() {
              String path = name.replace('.', '/').concat(".class");
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        return null;
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}

URLClassPath的getResource实现

首先先来看下URLClassPath重要属性

/* The original search path of URLs. */
private final ArrayList<URL> path;

/* The deque of unopened URLs */
private final ArrayDeque<URL> unopenedUrls;

/* The resulting search path of Loaders */
private final ArrayList<Loader> loaders = new ArrayList<>();

/* Map of each URL opened to its corresponding Loader */
private final HashMap<String, Loader> lmap = new HashMap<>();

如果传入URLStreamHandlerFactory参数,则默认为null, 然后主要构造的是path和unopenedUrls这两个URL的集合, jarHandler为空.loaders默认是空的集合。

public URLClassPath(URL[] urls, AccessControlContext acc) {
    this(urls, null, acc);
}
public URLClassPath(URL[] urls,
                    URLStreamHandlerFactory factory,
                    @SuppressWarnings("removal") AccessControlContext acc) {
    ArrayList<URL> path = new ArrayList<>(urls.length);
    ArrayDeque<URL> unopenedUrls = new ArrayDeque<>(urls.length);
    for (URL url : urls) {
        path.add(url);
        unopenedUrls.add(url);
    }
    this.path = path;
    this.unopenedUrls = unopenedUrls;
    if (factory != null) {
        jarHandler = factory.createURLStreamHandler("jar");
    } else {
        jarHandler = null;
    }
    if (DISABLE_ACC_CHECKING)
        this.acc = null;
    else
        this.acc = acc;
}

URLClassPath#getResource逻辑

  1. 遍历所有的Loader,然后调用getResource方法查询传入类全名的资源,如果找到则返回,没有则返回null。
public Resource getResource(String name, boolean check) {
    Loader loader;
    for (int i = 0; (loader = getLoader(i)) != null; i++) {
        Resource res = loader.getResource(name, check);
        if (res != null) {
            return res;
        }
    }
    return null;
}

URLClassPath#getLoader(int index)主要逻辑

  1. while循环, 其条件是loader集合大小小于index + 1,由于index是从0开始的,所以开始判断的时候条件是 0 < 1,条件判断为true,
  2. 循环体的逻辑如下:
    2.1 对unopenedUrls加锁,从unopenedUrls的数组的头部弹出出一个URL对象,如果没有了,则直接返回null.
    2.2 调用URLUtil#urlNoFragString函数对url转成字符串,以方便存入HashMap的key。如果lmap(即Map<String,Loader>类型)对象是否包含url转换后的字符串,说明已加载,则执行continue,结束当前循环。
    2.3 调用getLoader重载函数传入URL资源路径,将URL转换成Loader
    2.4 获取loader的classpath集合,如果不为空,则调用push函数将URL加入unopenedUrls的双端数组的头部.
    2.5 将新建的loader加入到loaders集合,同时加入lmap的key是ULR的字符换value 是Loader的Map集合。
  3. 循环体结束后,则取loader集合中index位置的Loader。
private synchronized Loader getLoader(int index) {
    if (closed) {
        return null;
    }
    while (loaders.size() < index + 1) {
        final URL url;
        synchronized (unopenedUrls) {
            url = unopenedUrls.pollFirst();
            if (url == null)
                return null;
        }
        String urlNoFragString = URLUtil.urlNoFragString(url);
        if (lmap.containsKey(urlNoFragString)) {
           continue;
        }
        Loader loader;
        try {
            loader = getLoader(url);
            URL[] urls = loader.getClassPath();
            if (urls != null) {
                push(urls);
            }
        } catch (IOException e) {
            // Silently ignore for now...
            continue;
        } catch (SecurityException se) {
          if (DEBUG) {
             System.err.println("Failed to access " + url + ", " + se );
          }
         continue;
        }
        // Finally, add the Loader to the search path.
        loaders.add(loader);
        lmap.put(urlNoFragString, loader);
    }
    return loaders.get(index);
}

URLClassPath#getLoader(URL url)主要逻辑如下
主要是将URL资源路径转换成Loader对象.直接getLoader中 PrivilegedExceptionAction的run函数的逻辑。

  1. 通过获取ULR的protocol协议以及资源文件名称.
  2. 判断文件名称不为空并且资源文件名称是'/'结尾的,如果为true,则继续判断URL的协议是'file'或则'jar'还是其他.
    2.1 协议为'file',则创建FileLoader.
    2.2 协议为'jar'并且默认url默认URLStreamHandler.则创建JarLoader.
    2.3 协议为其他,则创建Loader。
  3. 如果2中判断false,则创建JarLoader.
private Loader getLoader(final URL url) throws IOException {
    try {
        return AccessController.doPrivileged(
                new PrivilegedExceptionAction<>() {
                    public Loader run() throws IOException {
                        String protocol = url.getProtocol();
                        String file = url.getFile();
                        if (file != null && file.endsWith("/")) {
                            if ("file".equals(protocol)) {
                                return new FileLoader(url);
                            } else if ("jar".equals(protocol) &&
                                    isDefaultJarHandler(url) &&
                                    file.endsWith("!/")) {
                                // extract the nested URL
                                URL nestedUrl = new URL(file.substring(0, file.length() - 2));
                                return new JarLoader(nestedUrl, jarHandler, lmap, acc);
                            } else {
                                return new Loader(url);
                            }
                        } else {
                            return new JarLoader(url, jarHandler, lmap, acc);
                        }
                    }
                }, acc);
    } catch (PrivilegedActionException pae) {
        throw (IOException)pae.getException();
    }
}

那么接下我们主要以JarLoader为例分下、

JarLoader的getResource查询资源的实现

首先看下JarLoader的主要的字段,主要就是JarFile,

private static class JarLoader extends Loader {
    private JarFile jar;
    private final URL csu;
    private JarIndex index;
    private URLStreamHandler handler;
    private final HashMap<String, Loader> lmap;
    //省略
 }
  1. 首先调用ensureOpen确保jar文件已经解析完成。
  2. 通过调用JarFile的getJarEntry传入类全名,获取到JarEntry对象。
  3. 判断JarEntry不为空,则调用checkResource返回对应的资源。
  4. 判断JarIndex为空,则直接返回null。
  5. 如果jarentry为空,但是JarIndex不为空,则调用getResource查询类资源。
Resource getResource(final String name, boolean check) {
    try {
        ensureOpen();
    } catch (IOException e) {
        throw new InternalError(e);
    }
    final JarEntry entry = jar.getJarEntry(name);
    if (entry != null)
        return checkResource(name, check, entry);
    if (index == null)
        return null;
    HashSet<String> visited = new HashSet<>();
    return getResource(name, check, visited);
}
JarLoader#ensureOpen
  1. 首先通过getJarFile函数将传入的URL转换成JarFile对象.
  2. 判断JarFile中JarIndex不为空,则遍历JarIndex的JarFiles集合,将它拼接传入URL转成一个URL添加lmap集合。
private void ensureOpen() throws IOException {
    if (jar == null) {
        try {
                        jar = getJarFile(csu);
                        index = JarIndex.getJarIndex(jar);
                        if (index != null) {
                         String[] jarfiles = index.getJarFiles();
                         for (int i = 0; i < jarfiles.length; i++) {
                           try {
                      URL jarURL = new URL(csu, jarfiles[i]); 
            String urlNoFragString = URLUtil.urlNoFragString(jarURL);
          if (!lmap.containsKey(urlNoFragString)) {
                           lmap.put(urlNoFragString, null);
                        }
                     } catch (MalformedURLException e) {
                         //省略
        } catch (IOException e) {
            throw e.getException();
        }
    }
}
JarLoader#getJarFile
  1. 判断URL如果协议是'file',则直接走优化逻辑
    1.1 创建FileURLMapper对象传入URL.判断window平台上文件是否存在
    1.2 创建包装URL路径的File文件的JarFile.
  2. 否则调用URL的openConnect获取URLConnection,然后获取URLConnection的jarfile文件,这一步就是铜鼓远程下载jarFile文件.
private JarFile getJarFile(URL url) throws IOException {
    // Optimize case where url refers to a local jar file
    if (isOptimizable(url)) {
        FileURLMapper p = new FileURLMapper(url);
        if (!p.exists()) {
            throw new FileNotFoundException(p.getPath());
        }
        return checkJar(new JarFile(new File(p.getPath()), true, ZipFile.OPEN_READ,
                JarFile.runtimeVersion()));
    }
    URLConnection uc = (new URL(getBaseURL(), "#runtime")).openConnection();
    uc.setRequestProperty(USER_AGENT_JAVA_VERSION, JAVA_VERSION);
    JarFile jarFile = ((JarURLConnection)uc).getJarFile();
    return checkJar(jarFile);
}
JarFile的解析

首先来看下JarFile重要的Field字段
其中JarEntry是jar文件入口,Runtime.Version是运行时的版本。

public class JarFile extends ZipFile {
    private static final Runtime.Version BASE_VERSION;
    private static final int BASE_VERSION_FEATURE;
    private static final Runtime.Version RUNTIME_VERSION;
    private static final boolean MULTI_RELEASE_ENABLED;
    private static final boolean MULTI_RELEASE_FORCED;
    private static final ThreadLocal<Boolean> isInitializing = new ThreadLocal<>();

    private SoftReference<Manifest> manRef;
    private JarEntry manEntry;
    private JarVerifier jv;
    private boolean jvInitialized;
    private boolean verify;
    private final Runtime.Version version;  // current version
    private final int versionFeature;       // version.feature()

JarFile类是继承ZipFile,它本质上classes和资源压缩文件 JarFile的Specification参考 docs.oracle.com/javase/7/do…

JarFile构造函数\

  1. 初始化父类ZipFile构造函数,初始化file和mode。(mode上文中传入值值ZipFile.OPEN_READ)
  2. 赋值verify字段。(上文中传入的值为true)
  3. 赋值versionFeature字段。
public JarFile(File file, boolean verify, int mode, Runtime.Version version) throws IOException {
    super(file, mode);
    this.verify = verify;
    Objects.requireNonNull(version);
    //省略
    this.versionFeature = this.version.feature();
}

ZipFile构造函数

  1. 赋值name为传入File的path路径
  2. 赋值创建CleanableResource对象赋值给res字段
public ZipFile(File file, int mode, Charset charset) throws IOException
{                                            
    String name = file.getPath();
    file = new File(name);
    this.name = name;
    this.res = new CleanableResource(this, ZipCoder.get(charset), file, mode);
}

CleanableResource构造函数\

  1. 使用CleanerFactory创建一个Cleaner注册ZipFile对象以及CleanableResource自己,
  2. 创建空Set对象,这里是WeakHashSet集合,为了避免文件流没有引用后没有回收导致内存泄露。
  3. 创建inflaterCache数组.
  4. 创建Source对象.
CleanableResource(ZipFile zf, ZipCoder zc, File file, int mode) throws IOException {
    this.cleanable = CleanerFactory.cleaner().register(zf, this);
    this.istreams = Collections.newSetFromMap(new WeakHashMap<>());
    this.inflaterCache = new ArrayDeque<>();
    this.zsrc = Source.get(file, (mode & OPEN_DELETE) != 0, zc);
}

这里Cleaner对象主要为了利用虚引用机制清除istreams压缩文件集合、inflaterCache文件的集合、压缩文件Souce。 最后Souce类中就是包含对Jar文件格式解析,具体过程可以参考jarFil格式.

总结
本文主要分卸URLClassloader实现类加载,主要就是继承Classloader重写其findClass方法,并实现对URL资源查找,加载本地文件或远程的类资源文件到JVM中,解析成java.lang.Class对象。