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