为 Java 注册 classpath: 协议用 URL 读取文件

 本文为 Java 注册 classpath 协议读取文件的目的就是要让下面的代码能工作起来

假设在 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 时就能支持远程协议

以上代码能打印出 https://www.w3schools.com/xml/note.xml 的根节点是 "note",但是换成

试图读取 classpath 下的 /xml/note.xml 文件时出现前面一样的错误信息。因为 DocumentBuilder.parse(str) 本质上和

IOUtils.toString(new URL("classpath:/db.properties"), "UTF-8");

一样的方式来读取 URL 所指示的内容,因为本文接下来研究将使用读取 classpath:/... 文件内容成为可能。

题外话,在 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

假如以 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,可放在静态块中

这样的话,再执行最前面的代码

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")

通过 factory(这里是 TomcatURLStreamHandlerFactory) 创建 ClasspathURLStreamHandler 实例并使用它来读取 classpath 下的文件内空。

因此另一种方式我们也可定义自己的 URLStreamHandlerFactory, 然后在启动的时候用 URL.setURLStreamHandlerFactory(factory) 注册它。

或者如果是一个 Tomcat 应用程序,什么都不用做,Tomcat 启动的时候帮我们实现了。

用系统属性 -Djava.protocol.handler.pkgs 来注册 URLStreamHandler

应用这种方式包名和类名是有讲究的,比如我们创建一个 Handler 类,并放到  blog.yanbin.protocols.classpath 包下,Handler 类的内容如下(与前面相同)

然后执行下面的代码时

需要用指定系统属性

-Djava.protocol.handler.pkgs=blog.yanbin.protocols

强调一下包和类的名称匹配

  1. 自定义的 URLStreamHandler 类名必须命名为 Handler
  2. 包名为 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 等等。

类别: Java/JEE. 标签: , . 阅读(37). 订阅评论. TrackBack.
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x