本文为 Java 注册 classpath 协议读取文件的目的就是要让下面的代码能工作起来
1 2 |
String text = IOUtils.toString(new URL("classpath:/db.properties"), "UTF-8"); System.out.println(text); |
假设在 classpath 下有个文件 db.properties, 比如在 Maven 项目的 src/main/resources 目录中,或是在某个 jar 包的根位置。如果我们直接执行上面的代码将会得到异常
Exception in thread "main" java.net.MalformedURLException: unknown protocol: classpath
at java.net.URL.<init>(URL.java:617)
at java.net.URL.<init>(URL.java:507)
说是不认识的 classpath 协议。
前面代码是有实际用途的,比如说我们使用 XML 时就能支持远程协议
1 2 3 |
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder() .parse("https://www.w3schools.com/xml/note.xml"); System.out.println(document.getDocumentElement().getTagName()); |
以上代码能打印出 https://www.w3schools.com/xml/note.xml 的根节点是 "note",但是换成
1 2 |
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder() .parse("classpath:/xml/note.xml"); |
试图读取 classpath 下的 /xml/note.xml 文件时出现前面一样的错误信息。因为 DocumentBuilder.parse(str) 本质上和
IOUtils.toString(new URL("classpath:/db.properties"), "UTF-8");
一样的方式来读取 URL 所指示的内容,因为本文接下来研究将使用读取 classpath:/...
文件内容成为可能。
题外话,在 Spring 中我们可以通过配置
1 |
@PropertySource("classpath:/db.properties") |
来读取 classpath 下的 /db.properties,那么能不能借鉴它的实现呢?不能,它们是有区别的,Spring 的 @PropertySource 并不能读取远程 http(s) 指示的内容,它的实现只是用 ClassLoader 去加载 classpath 下的文件,请参考
1 2 3 |
else if (location.startsWith("classpath:")) { return new ClassPathResource(location.substring("classpath:".length()), getClassLoader()); } |
假如以 classpath:
开头,那么去除掉前缀用 ClassLoader 去读取 /db.properties
的内容。
回到最初,要使得 IOUtils.toString(new URL("classpath:/db.properties"), "UTF-8")
工作,我们需要为 classpath
协议注册一个 URLStreamHandler 实现,最终得以让 new URL("classpath:/db.properties").openConnection() 能工作。有两种方式可帮助我们注册 classpath 的 URLStreamHandler最简单的方式.
调用 URL 的静态方法 setURLStreamHandlerFactory
只要在程序启动的时候借助于 URL 的静态方法 setURLStreamHandlerFactory 来间接注册一个自己的 URLStreamHandler
,可放在静态块中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
static { URL.setURLStreamHandlerFactory(protocol -> "classpath".equals(protocol) ? new URLStreamHandler() { protected URLConnection openConnection(URL url) throws IOException { String path = url.getPath(); URL classpathUrl = Thread.currentThread().getContextClassLoader().getResource(path); if (classpathUrl == null) { classpathUrl = TestAppConfig.class.getResource(path); } if (classpathUrl == null) { throw new FileNotFoundException("classpathUrlStreamHandler.notFound: " + url); } else { return classpathUrl.openConnection(); } } } : null); } |
这样的话,再执行最前面的代码
1 2 |
String text = IOUtils.toString(new URL("classpath:/db.properties"), "UTF-8"); System.out.println(text); |
URL 就知道使用自定义的 URLStreamHandler 来处理 classpath 协议,取出 classpath 下 /db.properties 中的内容了。
这个实现是参考自 tomcat-embed-cor-x.x.xx.jar
中的 ClasspathURLStreamHandler
实现, 见 https://github.com/apache/tomcat/blob/main/java/org/apache/catalina/webresources/ClasspathURLStreamHandler.java#L27.
Tomcat 启动的时候,在它的 org.apache.catalina.util.LifecycleBase.start()
方法中辗转调用了 URL.setURLStreamHanlderFactory(factory)
把 factory
设置为 TomcatURLStreamHandlerFactory
, 其后如果协议是 classpath
的话,URL.getURLStreamHandler("classpath")
1 2 3 4 |
if (factory != null) { handler = factory.createURLStreamHandler(protocol); checkedWithFactory = true; } |
通过 factory(这里是 TomcatURLStreamHandlerFactory) 创建 ClasspathURLStreamHandler 实例并使用它来读取 classpath 下的文件内空。
因此另一种方式我们也可定义自己的 URLStreamHandlerFactory, 然后在启动的时候用 URL.setURLStreamHandlerFactory(factory)
注册它。
或者如果是一个 Tomcat 应用程序,什么都不用做,Tomcat 启动的时候帮我们实现了。
用系统属性 -Djava.protocol.handler.pkgs 来注册 URLStreamHandler
应用这种方式包名和类名是有讲究的,比如我们创建一个 Handler 类,并放到 blog.yanbin.protocols.classpath
包下,Handler 类的内容如下(与前面相同)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package blog.yanbin.protocols.classpath; import java.io.FileNotFoundException; import java.io.IOException; import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; public class Handler extends URLStreamHandler { protected URLConnection openConnection(URL url) throws IOException { String path = url.getPath(); URL classpathUrl = Thread.currentThread().getContextClassLoader().getResource(path); if (classpathUrl == null) { classpathUrl = Handler.class.getResource(path); } if (classpathUrl == null) { throw new FileNotFoundException("classpathUrlStreamHandler.notFound: " + url); } else { return classpathUrl.openConnection(); } } } |
然后执行下面的代码时
1 2 |
String text = IOUtils.toString(new URL("classpath:/db.properties"), StandardCharsets.UTF_8); System.out.println(text); |
需要用指定系统属性
-Djava.protocol.handler.pkgs=blog.yanbin.protocols
强调一下包和类的名称匹配
- 自定义的 URLStreamHandler 类名必须命名为 Handler
- 包名为 blog.yanbin.protocols.classpath 时用 -Djava.protocol.handler.pkgs 要指定为 blog.yanbin.protocols, 包名为
classpath
部分用来匹配要处理的协议,然后在其中找到 Handler 类。
如果想实现更多的协议,如 xyz, 就要创建一个类 blog.yanbin.protocols.xyz.Handler 类。
解析 -Djava.protocol.handler.pkgs 的实现
所有的逻辑同样是发生在 URL.getURLStreamHandler(String protocol) 方法中
通过 -Djava.protocol.handler.pkgs 指定 URLStreamHandler 的实现包,上面的 protocolPathProp
就是 java.protocol.handler.pkgs
,查找类名是用 packagePrefix + "." + protocol + ".Handler"
, 这就是为什么类为必须为 Handler
,并且要放置到下一级的 classpath
包中的原由。
同时还注意到 Java 总是把 sun.net.www.protocol
包附加到了 java.protocol.handler.pkgs
中,于是我们浏览一下这个包看看支持了一些什么协议
这就为什么我们一直可以用 file, ftp, http, https, jar, mailto 和 netdoc 协议,其中的 jar, mailto, netdoc 还没用过呢。
有了上面的基础后,想要自定义什么样的 URL 协议都不难了, 比如像 hadoop:/abc/xyz
, s3:/bucket_abc/xyz.avro
等等。
本文链接 https://yanbin.blog/register-classpath-protocol-for-java/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。