Featured image of post Java 类加载器(三)

Java 类加载器(三)

借助最近自己开发项目中遇到的问题,展开来说一下 java 类加载器在文件读取方面的使用

最近在使用netty开发一个简单的web服务,让其能够简单接收http的常见请求,方便以后使用。但是在开发过程中,遇到了一些资源读取方面的问题,故在这里记录一下,方面以后查看。

问题描述

在自定义一个类似spring@Controller注解的功能的时候,需要针对指定路径下的包以及其文件进行扫描,判断这些类文件中是否存在使用了@Controller的注解,如果有使用了该注解的,那么便将其判定为控制类(即Controller类)。为了实现这个功能,我先使用了java类加载器来加载文件,然后获取在指定包路径下所有类文件,其中读取包文件代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
List<Class> getClass(String packageName) throws ClassNotFoundException, IOException {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    String path = packageName.replace('.', '/');
    //通过类加载器获取中classpath路径下路径为path的文件
    Enumeration<URL> resources = classLoader.getResources(path);
    List<Class> classes = new ArrayList<>();
    while (resources.hasMoreElements()) {
        URL resource = resources.nextElement();
        classes.addAll(findClass(new File(resource.getFile()), packageName));
    }

    return classes;
}

List<Class> findClass(File directory, String packageName) throws ClassNotFoundException {
    List<Class> classes = new ArrayList<>();
    if (!directory.exists()) {
        return classes;
    }

    File[] files = directory.listFiles();
    if (files == null) {
        return classes;
    }
    for (File file : files) {
        if (file.isDirectory()) {
            assert !file.getName().contains(".");
            classes.addAll(findClass(file, packageName + "." + file.getName()));
        } else if (file.getName().endsWith(".class")) {
            classes.add(Class.forName(packageName + "." + file.getName().substring(0, file.getName().length() - 6)));
        }
    }
    return classes;
}

然而通过上面的方式获取类文件的时候,我发现在本地的时候的确可以获取到指定包路径下的所有类文件,但是一旦我将整个项目打包成jar包,那么就无法读取到指定包下面的类文件,这一点让我是十分不能理解。

问题深入

带着问题,我调试了一下这一块的代码,发现一个之前一直没有去考虑的现象,在本地运行的时候,当我执行到classLoader.getResources(path)的时候,通过resources.nextElement()方法获取到的是如下的信息

1
file:/home/root/gitwarehouse/ddns/ddns-web/target/classes/org/yinan/ddns/web/controller

而当我将项目打包之后再次运行的时候,通过resource.nextElement()方法获取到的信息发生了变化,变成了下面的信息:

1
jar:file:/home/root/gitwarehouse/ddns/dist/ddns-web-1.0.0/lib/ddns-web-1.0.0.jar!/org/yinan/ddns/web/controller

很明显,我们可以发现,在本地的时候我们读取的文件类型就是普通的文件,所以可以直接读取出我们需要的文件,但是,当在jar包中的时候,由于jar包会被算成一个整体来读取,即文件类型变成了jar,所以我们直接使用原来的代码进行读取,那么读取出来的一定是错误的。因此我们需要使用另外一种方式来针对jar文件进行读取。

问题解决

其实针对jar包的读取,ClassLoader中也已经存在了,我们可以中直接进行调用,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
 * 通过jar文件生成class
 * @param path 路径
 * @param resource 资源
 * @param classes 类集合
 * @throws IOException
 * @throws ClassNotFoundException
 */
void doScanPackageClassesByJar(String path, URL resource, List<Class> classes) throws IOException, ClassNotFoundException {
    JarFile jarFile = ((JarURLConnection)resource.openConnection()).getJarFile();
    Enumeration<JarEntry> entries = jarFile.entries();
    while (entries.hasMoreElements()) {
        JarEntry jarEntry = entries.nextElement();
        String name = jarEntry.getName();
        if (!name.startsWith(path) || jarEntry.isDirectory()) {
            continue;
        }
        if (name.lastIndexOf("/") != path.length() || name.indexOf('$') != -1) {
            continue;
        }
        if (name.endsWith(".class")) {
            name = name.replace("/", ".");
            name = name.substring(0, name.length() - 6);
            classes.add(Thread.currentThread().getContextClassLoader().loadClass(name));
        }
    }
}

我们只需要针对resource先读取jar包,然后再去jar包下读取一些文件即可,整体来说比较简单,但是针对问题的思路却显得比较重要。

扩展

通过上面的问题以及前两期管理类加载器的博客,我去查看了一下ClassLoader.class源码,其中关于方法getResources()源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public Enumeration<URL> getResources(String name) throws IOException {
    @SuppressWarnings("unchecked")
    Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
    if (parent != null) {
        //递归调用父加载器的该方法
        tmp[0] = parent.getResources(name);
    } else {
        //调用自己的获取系统层级的资源元素
        tmp[0] = getBootstrapResources(name);
    }
    //调用自定义的类加载器实现的加载方法
    tmp[1] = findResources(name);

    return new CompoundEnumeration<>(tmp);
}

其实在getResources()方法中也是使用了双亲委派模型,它依然是先去寻找是否有父类定义了这个方法,如果没有,那么自己才会进行处理,代码中有一行代码十分值得关注tmp[1] = findResources(name);,这一行代码也是去寻找指定名称的资源,但是findResources(name)方法确实一个需要子类自己实现的方法,所以如果我们有需要针对特定文件进行读取,那么我们完全可以在自己自定义的类加载器上面实现这个方法,这样当我们使用我们自己定义的类加载器加载指定类的时候,就可以使用findResources(name)方法进行读取。

详细代码

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus