什么是URL?
URL 即 Uniform Resource Locator,翻译为中文为统一资源定位符,表示万维网上的一个资源,资源可以是实际存在的一个文件,也可以是抽象的数据库的查询结果。
可以使用特定的字符串来表示这种资源,字符串格式为:protocol:userInfo@host:port/path?query#ref,如http://zzuhkp.com/index.html?key=abc#section1,具体含义如下:
protocol:访问资源的协议,如 http、ftp、file。
userInfo:访问资源的用户信息,该部分可选的,用户信息可能包含用户名和密码。
host:资源所在的主机,可以是 IP 地址或者主机名。
port:访问资源时使用的端口号,如果不存在,将使用协议默认的端口号。
path:资源所在的路径。
query:访问资源提供的附加查询参数,一般为键值对,键和值使用等号连接,多个键值对使用&连接,如果是中文还需要转为ASCII表示的形式,如 a=1&b=2。
ref:表示资源中的特定部分,如 html 页面中的锚点(Anchor)。
Java 中 URL 的表示
Java 提供了 java.net.URL 类表示统一资源定位符,统一资源定位符的目的是获取资源,因此 java.net.URL 同样也提供了获取资源的方法。
如何实例化 URL
先看如何实例化 URL,Java 提供了 URL 的几个构造方法用于创建 URL 对象的实例,具体如下。
/** * @param protocol 使用的协议名称 * @param host 主机名称 * @param port 主机的端口号 * @param file 主机上的资源,即 path * @param handler URL 的流处理器 */ public URL(String protocol, String host, int port, String file) throws MalformedURLException public URL(String protocol, String host, String file) throws MalformedURLException public URL(String protocol, String host, int port, String file,URLStreamHandler handler) throws MalformedURLException /** * @param context URL字符串的上下文 * @param spec 待解析的URL字符串,可能是一个相对路径 * @param handler URL的流处理器 */ public URL(String spec) throws MalformedURLException public URL(URL context, String spec) throws MalformedURLException public URL(URL context, String spec, URLStreamHandler handler) throws MalformedURLException
构造方法可以分为两类,一类构造方法参数是组成 URL 字符串的各部分,另一类构造方法参数是URL 字符串。从构造方法参数中我们还可以看到有一个类型为 URLStreamHandler 的参数,它和获取 URL 对应资源的流有关系。
如何获取 URL 的基本信息
创建 URL 实例对象时无论是直接提供组成 URL 字符串的各部分,还是提供完整的 URL 字符串,Java 中的 URL 对象都会在底层存储表示 URL 的各部分,并且提供了一些方法用于获取相关信息,具体如下。
public String getProtocol() public String getHost() public int getPort() public String getFile() public String getQuery() public String getAuthority() public String getPath() public String getUserInfo() public String getRef() public int getDefaultPort()
多数上述方法我们可以直接看出其获取的信息,而 authority 表示的是host:port,defaultPort 表示的是协议的默认端口。
如何获取 URL 表示的资源
URL 生来就是用来表示资源,因此 Java 中的 URL 也提供了一些方法用于获取资源内容,在 Java 中可以使用流来表示。URL 中获取资源的方法如下。
// 获取 URL 连接,连接对象可以获取更多的资源信息,如内容类型、内容长度、最后修改时间等。 public URLConnection openConnection() throws java.io.IOException // 通过代理获取 URL 连接 public URLConnection openConnection(Proxy proxy) throws java.io.IOException // 获取 URL 表示资源对应的输入流 public final InputStream openStream() throws java.io.IOException
可以看到,获取资源有两种方式,一种是通过 openConnection 方法先获取 URLConnection ,然后通过 URLConnection 获取到更多的资源信息,另一种方式是直接使用 openStream 获取表示 URL 资源的输入流,其底层调用 openConnection 方法进行实现。
例如,如果我们想获取到百度首页的信息,我们可以使用如下的代码。
public class App { public static void main(String[] args) throws IOException { String spec = "https://www.baidu.com/"; URL url = new URL(spec); URLConnection urlConnection = url.openConnection(); urlConnection.connect(); InputStream inputStream = urlConnection.getInputStream(); // 获取资源内容 String content = IoUtil.readUtf8(inputStream); System.out.println(content); inputStream.close(); } }
URL 扩展自定义协议
URL 可以表示各种协议的资源,那是不是所有的协议的资源都可以通过 URL 来获取呢?我们可以直接自定义一个 x 协议,然后通过 URL 获取吗?
答案是否定的,我们可以直接获取到 http 协议的资源内容,是因为 Java 中内置了对 http 协议资源的处理,处理的类就是 URLStreamHandler ,这个对象可以在调用构造方法实例化 URL 时提供。如果调用了不提供 URLStreamHandler 参数的构造方法,则需要使用一定的策略获取 URLStreamHandler 。
URL 资源获取实现分析
下面来分析,如何获取自定义协议的资源。跟踪 openConnection 方法的源码,具体如下。
public final class URL implements java.io.Serializable { transient URLStreamHandler handler; public URLConnection openConnection() throws java.io.IOException { return handler.openConnection(this); } }
我们看到,URLConnection 是通过 handler 来获取的,handler 是 URL 类中的成员变量, 其在构造方法执行的时候会进行实例化,具体如下。
public final class URL implements java.io.Serializable { public URL(String protocol, String host, int port, String file, URLStreamHandler handler) throws MalformedURLException { ... 省略部分代码 if (handler == null && (handler = getURLStreamHandler(protocol)) == null) { throw new MalformedURLException("unknown protocol: " + protocol); } this.handler = handler; } }
构造方法执行时为初始化 URLStreamHandler ,其调用了 getURLStreamHandler 方法根据协议来获取,继续跟踪源码。
public final class URL implements java.io.Serializable { static URLStreamHandlerFactory factory; private static final String protocolPathProp = "java.protocol.handler.pkgs"; static Hashtable<String,URLStreamHandler> handlers = new Hashtable<>(); private static Object streamHandlerLock = new Object(); static URLStreamHandler getURLStreamHandler(String protocol) { // 优先从缓存中获取 URLStreamHandler URLStreamHandler handler = handlers.get(protocol); if (handler == null) { boolean checkedWithFactory = false; if (factory != null) { // 缓存不存在,尝试从工厂中获取 URLStreamHandler handler = factory.createURLStreamHandler(protocol); checkedWithFactory = true; } // 工厂不存在或者未获取到 URLStreamHandler ,继续尝试获取 if (handler == null) { String packagePrefixList = null; // 从 Java 启动参数中获取 URLStreamHandler 类所在的包名 packagePrefixList = java.security.AccessController.doPrivileged( new sun.security.action.GetPropertyAction( protocolPathProp,"")); if (packagePrefixList != "") { packagePrefixList += "|"; } // 启动参数优先,另外加上特定的包名 packagePrefixList += "sun.net.www.protocol"; StringTokenizer packagePrefixIter = new StringTokenizer(packagePrefixList, "|"); while (handler == null && packagePrefixIter.hasMoreTokens()) { // 循环从包名列表中获取 URLStreamHandler 的实例 String packagePrefix = packagePrefixIter.nextToken().trim(); try { String clsName = packagePrefix + "." + protocol + ".Handler"; Class<?> cls = null; try { cls = Class.forName(clsName); } catch (ClassNotFoundException e) { ClassLoader cl = ClassLoader.getSystemClassLoader(); if (cl != null) { cls = cl.loadClass(clsName); } } if (cls != null) { handler = (URLStreamHandler)cls.newInstance(); } } catch (Exception e) { // any number of exceptions can get thrown here } } } // 加锁处理多线程安全问题 synchronized (streamHandlerLock) { URLStreamHandler handler2 = null; handler2 = handlers.get(protocol); if (handler2 != null) { return handler2; } if (!checkedWithFactory && factory != null) { handler2 = factory.createURLStreamHandler(protocol); } if (handler2 != null) { handler = handler2; } if (handler != null) { handlers.put(protocol, handler); } } } return handler; } }
根据上述代码,最终是调用了URLStreamHandler#openConnection(URL)获取 URLConnection,然后调用 URLConnection#getInputStream 方法获取表示资源的 InputStream, URLStreamHandler 的获取流程如下。
优先从缓存中获取 URLStreamHandler 。
缓存中不存在,尝试从 URLStreamHandlerFactory 工厂中获取 URLStreamHandler 。
如果工厂不存在或者没有从工厂中获取到 URLStreamHandler 则通过反射从启动命令中 java.protocol.handler.pkgs 参数指定的包名或者sun.net.www.protocol包名中根据命名规则,通过反射获取 URLStreamHandler 的实例。
最后将结果进行缓存。
如何在 URL 中扩展自定义协议
根据上述的分析,我们可以得知具有以下方式使 URL 支持自定义的协议。
实例化 URL 时指定 URLStreamHandler 。
为 URL 设置 URLStreamHandlerFactory ,设置方法为java.net.URL#setURLStreamHandlerFactory,注意该方法只能调用一次,再次调用将抛出异常。
在 Java 启动参数 java.protocol.handler.pkgs 中指定 URLStreamHandler 的包名,多个包名使用|分隔,或者将 URLStreamHandler 类放在包 sun.net.www.protocol 中,注意此时 URLStreamHandler 类的命名规则为 packagePrefix.protocol.Handler。
如果我们想要 URL 支持 x 协议,我们可以自定义 URLStreamHandler 如下。
package sun.net.www.protocol.x; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; /** * @author zzuhkp * @date 2020-11-16 14:52 * @since 1.0 */ public class Handler extends URLStreamHandler { @Override protected URLConnection openConnection(URL url) throws IOException { return new URLConnection(url) { @Override public void connect() throws IOException { } @Override public InputStream getInputStream() throws IOException { return new FileInputStream(url.getPath()); } }; } }
测试代码如下。
public class App { public static void main(String[] args) throws IOException { String spec = "x:D:\\Desktop\\新建文本文档.txt"; URL url = new URL(spec); URLConnection urlConnection = url.openConnection(); urlConnection.connect(); InputStream inputStream = urlConnection.getInputStream(); // 获取资源内容 String content = IoUtil.readUtf8(inputStream); System.out.println(content); inputStream.close(); } }
总结
本篇先介绍了 URL 的概念,然后介绍了如何在 Java 中获取 URL 中的信息及获取 URL 表示的资源,最后还分析了 URL 获取资源的源码及介绍如何让 URL 支持自定义的协议。URL 作为计算机领域中的基本概念,希望大家都能够掌握,欢迎大家留言讨论。