序
最近在使用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)
方法进行读取。
详细代码