我有一个非常简单的程序,它试图读取给定包下的类文件。这在本地(JDK 11)工作得很好,但是当我在Red Hat Linux 8服务器(JDK 11)上运行它时,相同的代码返回一个空数组[]
。
代码如下:
public static Set<Class> getClassesFromPackage(String packageName) throws ClassNotFoundException {
logger.debug(String.format("Getting classes from package %s", packageName));
InputStream stream = ClassLoader.getSystemClassLoader().getResourceAsStream(packageName.replaceAll("[.]", "/"));
logger.debug("stream: " + stream);
if (stream != null) {
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
return reader.lines()
.filter(line -> line.endsWith(".class"))
.map(line -> getClass(line, packageName))
.filter(clazz -> clazz != null)
.collect(Collectors.toSet());
} else {
throw new ClassNotFoundException(String.format("package name %s doesn't exist", packageName));
}
}
我这样调用它:
Set<Class> classes = getClassesFromPackage("foo");
logger.info("classes: " + classes);
在本地我得到以下内容:
classes: [class foo.Test]
在服务器上,我得到以下内容:
classes: []
没有错误被抛出或记录。
任何想法?
ClassLoader.getSystemClassLoader().getResourceAsStream(packageName.replaceAll("[.]", "/"));
这行不通。如果它能在windows上运行,那就……破碎的巧合。规范明确指出这是行不通的。
你想要的,是不可能的. 从根本上说,类加载器不支持"请给我包X中的每个类"的想法。他们就是不这么做。他们拥有的唯一原始元素是"给我资源X"。这就是你得到的。给定一个类名,如java.lang.String
,您可以从中获得其完全限定的二进制名称,并将其适当地斜线(java/lang/String.class
),然后请求该资源,这是ClassLoader API支持的事情。
但是,你不能拿着一个包裹要求清单。因为ClassLoader没有listResources
方法。您正在尝试"读取"资源java/lang
,并期望它返回文件列表。API规范并没有指出ClassLoader必须以这种方式工作,事实上,正如您现在发现的那样,大多数ClassLoader都没有这样做。
SPI是允许"列出"事物的解决方案,但SPI是一个可选择的系统,无论您使用java.util.ServiceLoader
与META-INF/services/com.foo.someInterfaceName
风格,还是在module-info.java
中使用较新的SPI东西-"提供者"必须选择被列出。
这让我们回到:不。你不能做你想做的事。
但是我需要
嗯,我想要一匹小马。还有世界和平。你唯一的选择就是黑进去。这意味着:每个新版本的java都可能会破坏你的代码,并且有可能(甚至可能)JVM版本,JVM提供程序(azul, openjdk, adoptium等)和操作系统的某些组合会破坏你的东西,并且没有人会接受你的错误报告,因为它不是一个错误。
如果你愿意接受这个巨大的头痛,你可以破解它。具体来说,你可以要求一个众所周知的资源,如.getResource("java/lang/String.class")
,toString()
的URL对象,你得到,然后去town:弄清楚这个URL是什么意思,并有代码的每一种资源URL,你期望知道如何解包该URL和做的工作。
classloader返回的URL可以是任何东西,包括data:
URL或自定义的blob://whatever
样式的URL。因此,这实际上是不可能的:您无法编写静态代码来响应任何人都可以编写自定义类加载器的世界。然而,绝大多数类加载器都有file
、jar
或jmod
url。因此,如果您为所有3种分发策略编写处理程序,则可以处理大多数分发策略,但不是所有分发策略。
file:
非常简单—转换为Path,去掉String.class
、lang
和java
部分,然后添加您感兴趣的包,现在您有了一个Path
对象,您可以将其扔到Files.newDirectoryStream
上,瞧,您有了您的类清单。这是而不是包中所有类的列表,这是不可能的。这仅仅是包中来自特定源的所有类的列表-可能还有更多(您可以在类路径中有两个条目,它们都具有相同的包)。这是很常见的,甚至,一个地方存放实际的代码,另一个地方存放同一包中的单元测试)。
对于jar:
url,找到!
,将jar:
和!
之间的内容转换为路径,将其作为new JarFile
打开,并查找具有正确包前缀的条目,然后您再次拥有您想要的内容。
对于jmod:
url,这有点棘手-但是jmod
工具存在并且可以列出jmod内容,关于如何读取jmod文件,您可能需要仔细阅读这个SO问题。
是的,那是很大的努力。是的,这就是重点——你正在解决一个基本问题,即类加载器API仅仅不允许你列出包内容尽管如此,你还是坚持这么做。这通常会导致"大量脆弱、难以维护的代码"。
另一种选择是找到一个库来为你做这些易碎的东西。也许反射库可以做到这一点。
注:很明显你是从这个牛粪教程中得到这句话的——通常牛粪是高质量的。这个条目。不是。