URL(统一资源定位符)简单的说就是通过一简单字符串就能定位到唯一资源,在Java中使用URL类表示。URL能够解析出protocol、hostname、port 等信息。 protocol 决定了交互规范,通用的协议,比如 HTTP 、 File 、 FTP 等协议, JDK 自带了默认的通讯实现。通过java.net.URL#openConnection()方法返回一个URLConnection来对代表的资源进行读写操作。
public URLConnection openConnection() throws java.io.IOException {
return handler.openConnection(this);
}
URLConnection是一个抽象类,URL#openConnection()返回的是它的实现类,具体是由通过URL构造参数中protocol选出来的一个URLStreamHandler负责创建,一类URLStreamHandler负责创建一类URLConnection实例。
1、选定URLStreamHandler
URL解析不是本文的重点,URL提供多个构造方法,这里就以相对简单的构造方法探究URLStreamHandler的选定过程。
public URL(String protocol, String host, int port, String file,
URLStreamHandler handler) throws MalformedURLException {
此处省略了解析出 protocol 、 hostname 、 port 等信息代码
///
// Note: we don't do full validation of the URL here. Too risky to change
// right now, but worth considering for future reference. -br
if (handler == null &&
//此方法是通过protocol选取handler的核心方法
(handler = getURLStreamHandler(protocol)) == null) {
throw new MalformedURLException("unknown protocol: " + protocol);
}
this.handler = handler;
//JDK提供了一系列内建的URLStreamHandler实现,都在sun.net.www.protocol子包下
if (host != null && isBuiltinStreamHandler(handler)) {
String s = IPAddressUtil.checkExternalForm(this);
if (s != null) {
throw new MalformedURLException(s);
}
}
//这里需要查看如果实现jar协议的是内建的sun.net.www.protocol.jar.Handler
//如果URL类似jar:jar:/file形式是不支持的
if ("jar".equalsIgnoreCase(protocol)) {
if (handler instanceof sun.net.www.protocol.jar.Handler) {
// URL.openConnection() would throw a confusing exception
// so generate a better exception here instead.
String s = ((sun.net.www.protocol.jar.Handler) handler).checkNestedProtocol(file);
if (s != null) {
throw new MalformedURLException(s);
}
}
}
}
上面代码中除了核心方法getURLStreamHandler()外,有一点值得注意就是对jar协议的处理,默认jdk使用内建的sun.net.www.protocol.jar.Handler来构建URLConnection,但是内建的实现不支持jar包中引用另一个jar协议的URL的entry,如果URL的全路径为jar:jar:file:/filepath/application.jar则抛出一个MalformedURLException快速报错。
下面是getURLStreamHandler方法核心逻辑:
static URLStreamHandler getURLStreamHandler(String protocol) {
URLStreamHandler handler = handlers.get(protocol);
if (handler == null) {
boolean checkedWithFactory = false;
// Use the factory (if any)
//如果通过静态方法setURLStreamHandlerFactory设置了URLStreamHandlerFactory则使用该工厂对象创建URLStreamHandler
if (factory != null) {
handler = factory.createURLStreamHandler(protocol);
checkedWithFactory = true;
}
// Try java protocol handler
if (handler == null) {
String packagePrefixList = null;
//读取系统属性java.protocol.handler.pkgs,如果属性不是空则使用'|'与内建协议实现包sun.net.www.protocol拼接
packagePrefixList
= java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
protocolPathProp,""));
if (packagePrefixList != "") {
packagePrefixList += "|";
}
// REMIND: decide whether to allow the "null" class prefix
// or not.
packagePrefixList += "sun.net.www.protocol";
//下面根据参数protocol和packagePrefixList拼接出sun.net.www.protocol.http.Handler格式的字符串,
//代表URLStreamHandler的类名进行无参实例化,如果实例化失败则使用'|'分割的后一个直到实例化成功或循环退出
StringTokenizer packagePrefixIter =
new StringTokenizer(packagePrefixList, "|");
while (handler == null &&
packagePrefixIter.hasMoreTokens()) {
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代码块
}
return handler;
}
由上面代码可知:java.net.URLStreamHandler 对象可以有两条途径得到:实现 java.net.URLStreamHandler ,或者实现 java.net.URLStreamHandlerFactory。
URL类提供了一个静态方法java.net.URL#setURLStreamHandlerFactory用来设置我们自定义的URLStreamHandlerFactory,随后通过getURLStreamHandler()方法返回URLStreamHandler优先使用这个工厂对象,如果未设置工厂对象或工厂对象返回为null,则根据我们对系统属性java.protocol.handler.pkgs配置的包名后面接‘.协议名.Handler'拼凑一个完整的类名使用无参构造方法进行实例化,这个代表完整类名的字符串支持同一个协议多个实现,只要类名之间使用分隔符'|'连接即可,如果实例化失败则使用该协议实现分隔符’|‘后一个类型进行实例化。
public static void setURLStreamHandlerFactory(URLStreamHandlerFactory fac) {
synchronized (streamHandlerLock) {
if (factory != null) {
throw new Error("factory already defined");
}
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkSetFactory();
}
handlers.clear();
factory = fac;
}
}
如果我们既没有提供URLStreamHandlerFactory和java.protocol.handler.pkgs系统属性getURLStreamHandler()方法提供了一个内建的实现包sun.net.www.protocol,打开这个包可以看到JDK提供的file、ftp、http等协议的实现。
sun.net.www.protocol.jar.Handler的openConnection()方法返回的则是相同包下的JarURLConnection。
2、JarURLConnection实现
上面讲到通过sun.net.www.protocol.jar.Handler得到一个JarURLConnection,这个对象主要提供了两个方法分别获取URL表示的JarEntry和JarFile。
public JarFile getJarFile() throws IOException {
this.connect();
return this.jarFile;
}
public JarEntry getJarEntry() throws IOException {
this.connect();
return this.jarEntry;
}
这个JarURLConnection继承自java.net.JarURLConnection,java.net.JarURLConnection提供了获取jarFileURL和entryName的方法,而解析出这两个属性是在构造方法中完成的。
protected JarURLConnection(URL url) throws MalformedURLException {
super(url);
parseSpecs(url);
}
private void parseSpecs(URL url) throws MalformedURLException {
String spec = url.getFile();
//jar协议的URL必须包含!/作为jarfile与jarentry的分界
int separator = spec.indexOf("!/");
/*
* REMIND: we don't handle nested JAR URLs
*/
if (separator == -1) {
throw new MalformedURLException("no !/ found in url spec:" + spec);
}
jarFileURL = new URL(spec.substring(0, separator++));
entryName = null;
/* if ! is the last letter of the innerURL, entryName is null */
if (++separator != spec.length()) {
entryName = spec.substring(separator, spec.length());
entryName = ParseUtil.decode (entryName);
}
}
parseSpecs()方法很简单就是讲url分解出jarFileURL和entryName,例如URL("jar:http://www.foo.com/bar/baz.jar!/COM/foo/Quux.class")的jarFile=http://www.foo.com/bar/baz.jar,entryName=COM/foo/Quux.class,这里有一点需要注意即使url定位的是一个jar文件而非jar entry,结尾的"!/"是不可少的,否则抛出new MalformedURLException("no !/ found in url spec:" + spec)。
回过头再来看sun.net.www.protocol.jar.JarURLConnection的构造方法。
public JarURLConnection(URL var1, Handler var2) throws MalformedURLException, IOException {
super(var1);
this.jarFileURLConnection = this.jarFileURL.openConnection();
this.entryName = this.getEntryName();
}
在这个构造方法中,通过使用父类解析出的jarFileURL得到可以访问该文件的jarFileURLConnection,因为jarFileURL本身也是一个URL对象调用openConnection()方法,其具体使用哪个URLStreamHandler依然取决于protocol,如上面例子中jarFile=http://www.foo.com/bar/baz.jar,则使用sun.net.www.protocol.http.Handler返回一个sun.net.www.protocol.http.HttpURLConnection。在得到jarFileURLConnection后再来分析一下本节开头说的getJarFile()和getJarEntry()实现。
这两个方法都是在调用自身connect()方法后直接返回该成员变量则jarFile也是在connect()方法中完成解析的。
public void connect() throws IOException {
if (!this.connected) {
this.jarFile = factory.get(this.getJarFileURL(), this.getUseCaches());
if (this.getUseCaches()) {
boolean var1 = this.jarFileURLConnection.getUseCaches();
this.jarFileURLConnection = factory.getConnection(this.jarFile);
this.jarFileURLConnection.setUseCaches(var1);
}
if (this.entryName != null) {
this.jarEntry = (JarEntry)this.jarFile.getEntry(this.entryName);
if (this.jarEntry == null) {
try {
if (!this.getUseCaches()) {
this.jarFile.close();
}
} catch (Exception var2) {
}
throw new FileNotFoundException("JAR entry " + this.entryName + " not found in " + this.jarFile.getName());
}
}
this.connected = true;
}
}
从connect()方法得知,jarFile是通过JarFileFactory对象get方法获取的,entryName则直接使用jarFile的成员方法,那么来看sun.net.www.protocol.jar.JarFileFactory的get方法。
JarFile get(URL var1, boolean var2) throws IOException {
JarFile var3;
if (var2) {
synchronized(instance) {
var3 = this.getCachedJarFile(var1);
}
if (var3 == null) {
JarFile var4 = URLJarFile.getJarFile(var1, this);
synchronized(instance) {
var3 = this.getCachedJarFile(var1);
if (var3 == null) {
fileCache.put(URLUtil.urlNoFragString(var1), var4);
urlCache.put(var4, var1);
var3 = var4;
} else if (var4 != null) {
var4.close();
}
}
}
} else {
var3 = URLJarFile.getJarFile(var1, this);
}
if (var3 == null) {
throw new FileNotFoundException(var1.toString());
} else {
return var3;
}
}
上面方法除了提供缓存机制,实际获取JarFile对象是通过sun.net.www.protocol.jar.URLJarFile#getJarFile(java.net.URL, sun.net.www.protocol.jar.URLJarFile.URLJarFileCloseController)方法。
static JarFile getJarFile(URL var0, URLJarFile.URLJarFileCloseController var1) throws IOException {
return (JarFile)(isFileURL(var0) ? new URLJarFile(var0, var1) : retrieve(var0, var1));
}
如果jarFileUrl是以file开头则直接使用URLJarFile、否则调用retrieve方法,而在retrieve方法中通过jarFileUrl相应的UrlConnection获取一个输入流在导入URLJarFile中,而URLJarFile继承于java.util.jar.JarFile,拥有JarFile的方法,而JarFile继承于ZipFile,可见jar文件就是一种zip文件,只不过内部包含了一个特殊的entry,并提供了访问该entry的方法getManEntry()。
public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
private JarEntry getManEntry() {
if (manEntry == null) {
// First look up manifest entry using standard name
manEntry = getJarEntry(MANIFEST_NAME);
if (manEntry == null) {
// If not found, then iterate through all the "META-INF/"
// entries to find a match.
String[] names = getMetaInfEntryNames();
if (names != null) {
for (int i = 0; i < names.length; i++) {
if (MANIFEST_NAME.equals(
names[i].toUpperCase(Locale.ENGLISH))) {
manEntry = getJarEntry(names[i]);
break;
}
}
}
}
}
return manEntry;
}