Java URL协议实现扩展原理

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;
}
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值