URL 源码分析
需要了解的知识点:
URI、 URL 和 URN 的区别
URI 源码分析
URL 和URI的最大区别是:
URL可以定位到一个资源,也就是说,URL类可以访问URL指定的资源信息。
URI只是标识一个对象,所以URI类无法获取URI标识的对象。
下面通过源码来分析URL类的实现细节:
构造
public URL(String spec);
public URL(String protocol, String host, String file);
public URL(String protocol, String host, int port, String file)
public URL(String protocol, String host, int port, String file,
URLStreamHandler handler);
public URL(URL context, String spec);
public URL(URL context, String spec, URLStreamHandler handler);
URL提供了6种不同的构造方法,使用那个构造方法取决你有那些信息以及信息形式。
- 根据一个字符串形式的URL,来构建URL对象。
- 根据 协议、主机名、文件来构造一个URL。
使用该协议默认的端口,并且file参数应当以斜线开头,包括文件路径、文件名称和片段。 - 根据 协议、主机名、端口、文件来构造一个URL。
- 根据 协议、主机名、端口、文件和URLStreamHandler来构造一个URL。
URLStreamHandler:主要是用来读取指定的资源,并返回该资源的一个流。 - 根据一个基础URL和一个相对URL来构建一个绝对URL。
- 根据一个基础URL和一个相对URL来构建一个绝对URL,并传入一个URLStreamHandler对象。
解析URL
URL主要是通过7部分组成,如下图:

-
获得URL的协议
public String getProtocol() -
获得授权机构信息(包括用户信息、主机和端口)
public String getAuthority() -
获得用户信息(用户名和密码)
public String getUserInfo() -
获取主机地址(域名或ip地址)
public String getHost() -
获得端口
public int getPort() -
获得文件信息(路径、文件名和查询参数)
public String getFile() -
获得路径信息(路径、文件名)
public String getPath() -
获取查询参数信息
public String getQuery() -
获得片段信息
public String getRef() -
获得该协议默认端口
public int getDefaultPort()
解析URL 示例
URL url = new URL("http://user:pass@localhost:8080/infcn/index.html?type=type1#aaa");
System.out.println("protocol :\t"+url.getProtocol());
System.out.println("authority :\t"+url.getAuthority());
System.out.println("userinfo :\t"+url.getUserInfo());
System.out.println("host :\t"+url.getHost());
System.out.println("port :\t"+url.getPort());
System.out.println("file :\t"+url.getFile());
System.out.println("path :\t"+url.getPath());
System.out.println("query :\t"+url.getQuery());
System.out.println("ref :\t"+url.getRef());
System.out.println("defaultport:\t"+url.getDefaultPort());

URL 获取数据
从概念上区分:URI只是标识一个资源,而URL可以定位一个资源。
所以java总URI只负责解析URI功能,而URL有解析URL的功能,还有获取URL指定资源的数据。
可以通过以下5个方法来获取URL指定的资源数据
public URLConnection openConnection();
public URLConnection openConnection(Proxy proxy);
public final InputStream openStream();
public final Object getContent();
public final Object getContent(Class[] classes);
openConnection()方法
public URLConnection openConnection() throws java.io.IOException {
return handler.openConnection(this);
}
直接调用URLStreamHandler.openConnection()方法获取URLConnection对象。URLConnection 对象可以获取原始的文档(如:html、纯文本、二进制图像等),还可以获取访问这个协议指定的所有的元数据(如:http协议的请求头信息)。URLConnection 对象除了从URL中读取资源外,还允许向URL中写入数据。(如:http post提交表单数据,mailto 发送电子邮件等)
URLStreamHandler可以让系统根据当前URL协议来选择响应的Handler,也可以使用扩展URLStreamHandler类来自定义实现相应资源的获取,也可以扩展协议。
URLStreamHandler 类结构
public abstract class URLStreamHandler{
abstract protected URLConnection openConnection(URL u) throws IOException;
protected URLConnection openConnection(URL u, Proxy p) throws IOException {
throw new UnsupportedOperationException("Method not implemented.");
}
protected void parseURL(URL u, String spec, int start, int limit){
...
}
protected int getDefaultPort() {
return -1;
}
protected boolean equals(URL u1, URL u2) {
String ref1 = u1.getRef();
String ref2 = u2.getRef();
return (ref1 == ref2 || (ref1 != null && ref1.equals(ref2))) &&
sameFile(u1, u2);
}
protected int hashCode(URL u){
...
}
protected boolean sameFile(URL u1, URL u2) {
// Compare the protocols.
if (!((u1.getProtocol() == u2.getProtocol()) ||
(u1.getProtocol() != null &&
u1.getProtocol().equalsIgnoreCase(u2.getProtocol()))))
return false;
// Compare the files.
if (!(u1.getFile() == u2.getFile() ||
(u1.getFile() != null && u1.getFile().equals(u2.getFile()))))
return false;
// Compare the ports.
int port1, port2;
port1 = (u1.getPort() != -1) ? u1.getPort() : u1.handler.getDefaultPort();
port2 = (u2.getPort() != -1) ? u2.getPort() : u2.handler.getDefaultPort();
if (port1 != port2)
return false;
// Compare the hosts.
if (!hostsEqual(u1, u2))
return false;
return true;
}
......
}
由此类可以看出,子类必须覆盖的方法是openConnection(URL u)方法,如果该协议支持代理模式,则也需要覆盖openConnection(URL u, Proxy p)方法。
URLStreamHandler 实例化
如果用户传过来的URLStreamHandler 实例,则需要验证安全性问题。比如:applet程序是在客户的浏览器端运行的java程序,他如果读取服务器上jar文件的时候就可以通过,如果读取客户端本地磁盘中的文件则不允许访问。代码如下图:

如果用户没有指定URLStreamHandler实例,则通过protocol协议来决定使用哪个协议的URLStreamHandler的实例。代码如下:

jdk的sun.net.www.protocol包中默认支持以下几种协议,当然用户也可以扩展URLStreamHandler实例,来实现自定义的协议。
java中默认支持的协议如下图:

openConnection(Proxy proxy) 方法
public URLConnection openConnection(Proxy proxy) throws java.io.IOException {
if (proxy == null) {
throw new IllegalArgumentException("proxy can not be null");
}
// Create a copy of Proxy as a security measure
Proxy p = proxy == Proxy.NO_PROXY ? Proxy.NO_PROXY : sun.net.ApplicationProxy.create(proxy);
SecurityManager sm = System.getSecurityManager();
if (p.type() != Proxy.Type.DIRECT && sm != null) {
InetSocketAddress epoint = (InetSocketAddress) p.address();
if (epoint.isUnresolved())
sm.checkConnect(epoint.getHostName(), epoint.getPort());
else
sm.checkConnect(epoint.getAddress().getHostAddress(), epoint.getPort());
}
return handler.openConnection(this, p);
}
可以通过URL对象设置的代理来获取URLConnection对象。
openStream() 方法
public final InputStream openStream() throws java.io.IOException {
return openConnection().getInputStream();
}
直接获取URL指定资源的流InputStream对象,该方法无法向URL中写如数据,也无法访问这个协议的所有的元数据(如:html协议的请求头)。
getContent() 方法
public final Object getContent() throws java.io.IOException {
return openConnection().getContent();
}
getContent() 方法返回由URL引用的数据,尝试由它建立某种类型的对象。如果URL指定的资源是文本类型(如:html、asciii 文件),返回的就是InputStream对象。如果URL指定的资源是图片则返回java.awt.ImageProducer对象。
getContent(Class[] classes) 方法
final Object getContent(Class[] classes) throws java.io.IOException {
return openConnection().getContent(classes);
}
该方法允许用户选择希望将内容作为那个类型返回。
例如,如果首先将HTML文件作为一个String返回,而第二个选择是Reader,第三个选择是InputStream,就可以编写一下代码:
URL u = new URL("http://www.jijianshuai.com");
Class<?> types = new Class[3];
types[0] = String.class;
types[1] = Reader.class;
types[2] = InputStream.class;
Object obj = u.getContent(types);
equals 方法
URL类的equals方法是调用URLStreamHandler对象的equals方法。
protected boolean equals(URL u1, URL u2) {
String ref1 = u1.getRef();
String ref2 = u2.getRef();
return (ref1 == ref2 || (ref1 != null && ref1.equals(ref2))) &&
sameFile(u1, u2);
}
equals方法会对比 URL指向的主机、端口、文件路径和片段标识。当所有的都一样才会返回true,但equals方法也会尝试解析DNS,来判断两个主机是否相同。如:可以判断http://localhost:8080/index.html 和 http://127.0.0.1:8080/index.html 两个URL是相等的。
equals方法底层是调用了sameFile()方法。
注意:
因为equals方法有解析DNS的功能,解析DNS是一个阻塞IO操作!所以应当避免URL存储在依赖equals()的数据结构中,如HashMap。如果要存储最后是使用URI来进行存储。URI的equals方法是不会解析DNS的。
sameFile(URL u1, URL u2) 方法
该方法作用和equals基本相同,这里也包括DNS解析,不过sameFile()不考虑片段标识问题。下面通过代码来解析sameFIle的实现。
protected boolean sameFile(URL u1, URL u2) {
if (!((u1.getProtocol() == u2.getProtocol()) ||
(u1.getProtocol() != null &&
u1.getProtocol().equalsIgnoreCase(u2.getProtocol()))))
return false;
if (!(u1.getFile() == u2.getFile() ||
(u1.getFile() != null && u1.getFile().equals(u2.getFile()))))
return false;
int port1, port2;
port1 = (u1.getPort() != -1) ? u1.getPort() : u1.handler.getDefaultPort();
port2 = (u2.getPort() != -1) ? u2.getPort() : u2.handler.getDefaultPort();
if (port1 != port2)
return false;
if (!hostsEqual(u1, u2))
return false;
return true;
}
- 判断两个URL的协议是否一致
- 判断两个URL的获得文件信息是否一致。文件信息包括:路径、文件名和查询参数
- 判断两个URL的端口是否一致
- 判断host是否一致。调用hostsEqual方法来判断。
hostsEqual(URL u1, URL u2) 方法
protected boolean hostsEqual(URL u1, URL u2) {
InetAddress a1 = getHostAddress(u1);
InetAddress a2 = getHostAddress(u2);
// if we have internet address for both, compare them
if (a1 != null && a2 != null) {
return a1.equals(a2);
// else, if both have host names, compare them
} else if (u1.getHost() != null && u2.getHost() != null)
return u1.getHost().equalsIgnoreCase(u2.getHost());
else
return u1.getHost() == null && u2.getHost() == null;
}
该方法使用两个URL的host来构造InetAddress对象,调用InetAddress对象的getHost() 方法 来判断两个host是否一致。
InetAddress.getHost()可以通过DNS解析域名,来获取域名绑定的ip地址。
想了解更多精彩内容请关注我的公众号
