实现ResourceResolver

廖雪峰
资深软件开发工程师,业余马拉松选手。

在编写IoC容器之前,我们首先要实现@ComponentScan,即解决“在指定包下扫描所有Class”的问题。

Java的ClassLoader机制可以在指定的Classpath中根据类名加载指定的Class,但遗憾的是,给出一个包名,例如,org.example,它并不能获取到该包下的所有Class,也不能获取子包。要在Classpath中扫描指定包名下的所有Class,包括子包,实际上是在Classpath中搜索所有文件,找出文件名匹配的.class文件。例如,Classpath中搜索的文件org/example/Hello.class就符合包名org.example,我们需要根据文件路径把它变为org.example.Hello,就相当于获得了类名。因此,搜索Class变成了搜索文件。

我们先定义一个Resource类型表示文件:

public record Resource(String path, String name) {
}

再仿造Spring提供一个ResourceResolver,定义scan()方法来获取扫描到的Resource

public class ResourceResolver {
    String basePackage;

    public ResourceResolver(String basePackage) {
        this.basePackage = basePackage;
    }

    public <R> List<R> scan(Function<Resource, R> mapper) {
        ...
    }
}

这样,我们就可以扫描指定包下的所有文件。有的同学会问,我们的目的是扫描.class文件,如何过滤出Class?

注意到scan()方法传入了一个映射函数,我们传入Resource到Class Name的映射,就可以扫描出Class Name:

// 定义一个扫描器:
ResourceResolver rr = new ResourceResolver("org.example");
List<String> classList = rr.scan(res -> {
    String name = res.name(); // 资源名称"org/example/Hello.class"
    if (name.endsWith(".class")) { // 如果以.class结尾
        // 把"org/example/Hello.class"变为"org.example.Hello":
        return name.substring(0, name.length() - 6).replace("/", ".").replace("\\", ".");
    }
    // 否则返回null表示不是有效的Class Name:
    return null;
});

这样,ResourceResolver只负责扫描并列出所有文件,由客户端决定是找出.class文件,还是找出.properties文件。

在ClassPath中扫描文件的代码是固定模式,可以在网上搜索获得,例如StackOverflow的这个回答。这里要注意的一点是,Java支持在jar包中搜索文件,所以,不但需要在普通目录中搜索,也需要在Classpath中列出的jar包中搜索,核心代码如下:

// 通过ClassLoader获取URL列表:
Enumeration<URL> en = getContextClassLoader().getResources("org/example");
while (en.hasMoreElements()) {
    URL url = en.nextElement();
    URI uri = url.toURI();
    if (uri.toString().startsWith("file:")) {
        // 在目录中搜索
    }
    if (uri.toString().startsWith("jar:")) {
        // 在Jar包中搜索
    }
}

几个要点:

  1. ClassLoader首先从Thread.getContextClassLoader()获取,如果获取不到,再从当前Class获取,因为Web应用的ClassLoader不是JVM提供的基于Classpath的ClassLoader,而是Servlet容器提供的ClassLoader,它不在默认的Classpath搜索,而是在/WEB-INF/classes目录和/WEB-INF/lib的所有jar包搜索,从Thread.getContextClassLoader()可以获取到Servlet容器专属的ClassLoader;
  2. Windows和Linux/macOS的路径分隔符不同,前者是\,后者是/,需要正确处理;
  3. 扫描目录时,返回的路径可能是abc/xyz,也可能是abc/xyz/,需要注意处理末尾的/

这样我们就完成了能扫描指定包以及子包下所有文件的ResourceResolver

参考源码

可以从GitHubGitee下载源码。

GitHub



Comments

Loading comments...