Java中从类路径加载资源的URL

216

在Java中,您可以使用不同的URL协议使用相同的API加载各种资源:

file:///tmp.txt
http://127.0.0.1:8080/a.properties
jar:http://www.foo.com/bar/baz.jar!/COM/foo/Quux.class

这种方法很好地将资源的实际加载与需要该资源的应用程序分离开来,由于URL只是一个字符串,因此资源加载也非常容易配置。

是否有一种协议可以使用当前的类加载器来加载资源?这类似于Jar协议,但我不需要知道资源来自哪个jar文件或类文件夹。

我当然可以使用Class.getResourceAsStream("a.xml")来完成,但这将需要我使用不同的API,从而改变现有的代码。我希望能够在所有可以已经指定资源URL的地方使用它,只需更新属性文件即可。

14个回答

367

介绍和基本实现

首先,您至少需要一个URLStreamHandler。 这将实际打开到给定URL的连接。 请注意,它简单地称为Handler;这允许您指定java -Djava.protocol.handler.pkgs=org.my.protocols,并且它将自动被拾取,使用“simple”包名称作为支持的协议(在此例中是“classpath”)。

用法

new URL("classpath:org/my/package/resource.extension").openConnection();

代码

package org.my.protocols.classpath;

import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;

/** A {@link URLStreamHandler} that handles resources on the classpath. */
public class Handler extends URLStreamHandler {
    /** The classloader to find resources from. */
    private final ClassLoader classLoader;

    public Handler() {
        this.classLoader = getClass().getClassLoader();
    }

    public Handler(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    @Override
    protected URLConnection openConnection(URL u) throws IOException {
        final URL resourceUrl = classLoader.getResource(u.getPath());
        return resourceUrl.openConnection();
    }
}

启动问题

如果你和我一样,不想依赖于在启动时设置属性来让你到达某个地方(在我这种情况下,我喜欢保持我的选择开放,比如Java WebStart - 这就是为什么需要所有这些)。

解决方法/增强功能

手动代码处理程序规范

如果你掌控着代码,你可以做:

new URL(null, "classpath:some/package/resource.extension", new org.my.protocols.classpath.Handler(ClassLoader.getSystemClassLoader()))

这将使用您的处理程序打开连接。

但是,这还不够令人满意,因为您不需要URL来执行此操作 - 您想要这样做是因为某个库希望使用URL,而您无法或不想控制...

JVM处理程序注册

最终选项是注册一个URLStreamHandlerFactory,它将处理JVM中的所有URL:

package my.org.url;

import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.util.HashMap;
import java.util.Map;

class ConfigurableStreamHandlerFactory implements URLStreamHandlerFactory {
    private final Map<String, URLStreamHandler> protocolHandlers;

    public ConfigurableStreamHandlerFactory(String protocol, URLStreamHandler urlHandler) {
        protocolHandlers = new HashMap<String, URLStreamHandler>();
        addHandler(protocol, urlHandler);
    }

    public void addHandler(String protocol, URLStreamHandler urlHandler) {
        protocolHandlers.put(protocol, urlHandler);
    }

    public URLStreamHandler createURLStreamHandler(String protocol) {
        return protocolHandlers.get(protocol);
    }
}

要注册处理程序,请使用配置的工厂调用URL.setURLStreamHandlerFactory()。然后像第一个示例一样执行new URL("classpath:org/my/package/resource.extension"),就可以开始了。

JVM处理程序注册问题

请注意,每个JVM只能调用此方法一次,并且请注意Tomcat将使用此方法注册JNDI处理程序(AFAIK)。尝试Jetty(我会尝试);在最坏的情况下,您可以先使用该方法,然后它必须围绕您工作!

许可证

我将其发布到公共领域,并要求如果您希望进行修改,请在某处启动OSS项目并在此处注释详细信息。更好的实现是具有URLStreamHandlerFactory的线程本地存储URLStreamHandler,以存储每个Thread.currentThread().getContextClassLoader()的处理程序。我甚至会提供我的修改和测试类。


1
@Stephen 这正是我正在寻找的。你能否与我分享你的更新?我可以将其作为我的 com.github.fommil.common-utils 包的一部分,计划通过 Sonatype 进行更新和发布。 - fommil
5
请注意,您也可以使用System.setProperty()来注册协议。例如:System.setProperty("java.protocol.handler.pkgs", "org.my.protocols"); - tsauerwein
Java 9+ 有一种更简单的方法:https://dev59.com/tHRA5IYBdhLWcg3wsgFP#56088592 - mhvelplund

112
URL url = getClass().getClassLoader().getResource("someresource.xxx");

那应该就可以了。


12
我可以使用 Class.getResourceAsStream("a.xml") 来做到这一点,但那将需要我使用不同的 API,因此需要更改现有的代码。我希望能够在所有可以指定资源 URL 的地方使用它,只需更新属性文件即可。 - Thilo
3
正如Thilo指出的,这是原帖作者曾考虑但拒绝的事情。 - sleske
16
getResource和getResourceAsStream是不同的方法。虽然同意getResourceAsStream不符合API,但getResource返回一个URL,这正是OP所要求的。 - romacafe
getResource() 不会从类路径中读取,它是从文件系统中读取。 - Halil
3
OP要求属性文件解决方案,但其他人也因为问题标题而来到这里。他们喜欢这个动态解决方案 :) - Jarekczek
显示剩余3条评论

16

我认为这值得单独回答 - 如果您正在使用Spring,则已经具备此功能,方法是

Resource firstResource =
    context.getResource("http://www.google.fi/");
Resource anotherResource =
    context.getResource("classpath:some/resource/path/myTemplate.txt");

正如skaffman在评论中指出的那样,spring文档中所解释的一样。


2
在我看来,Spring的ResourceLoader.getResource()更适合这个任务(ApplicationContext.getResource()在内部委托给它)。 - Ilya Serbis

13

从Java 9及以上版本开始,您可以定义一个新的URLStreamHandlerProviderURL类在运行时使用服务加载器框架来加载它。

创建提供程序:

package org.example;

import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.net.spi.URLStreamHandlerProvider;

public class ClasspathURLStreamHandlerProvider extends URLStreamHandlerProvider {

    @Override
    public URLStreamHandler createURLStreamHandler(String protocol) {
        if ("classpath".equals(protocol)) {
            return new URLStreamHandler() {
                @Override
                protected URLConnection openConnection(URL u) throws IOException {
                    return ClassLoader.getSystemClassLoader().getResource(u.getPath()).openConnection();
                }
            };
        }
        return null;
    }

}
META-INF/services 目录下创建一个名为 java.net.spi.URLStreamHandlerProvider 的文件,并将以下内容写入其中:
org.example.ClasspathURLStreamHandlerProvider

现在,当URL类看到类似如下内容时,它将使用提供程序:

Now the URL class will use the provider when it sees something like:

URL url = new URL("classpath:myfile.txt");

10

您还可以在启动期间通过编程方式设置属性:

final String key = "java.protocol.handler.pkgs";
String newValue = "org.my.protocols";
if (System.getProperty(key) != null) {
    final String previousValue = System.getProperty(key);
    newValue += "|" + previousValue;
}
System.setProperty(key, newValue);

使用这个类:

package org.my.protocols.classpath;

import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;

public class Handler extends URLStreamHandler {

    @Override
    protected URLConnection openConnection(final URL u) throws IOException {
        final URL resourceUrl = ClassLoader.getSystemClassLoader().getResource(u.getPath());
        return resourceUrl.openConnection();
    }
}

这样做是最不会干扰其他功能的方法。 :) java.net.URL 总是使用系统属性的当前值。


2
只有当处理程序旨在处理尚未“已知”的协议(例如gopher://)时,才可以使用添加额外包以进行查找的代码来添加到java.protocol.handler.pkgs系统变量。如果意图是覆盖“流行”的协议,如file://http://,则可能为时已晚,因为java.net.URL#handlers映射已经为该协议添加了一个“标准”处理程序。因此,唯一的出路是将此变量传递给JVM。 - dma_k

6

Azder的回答类似,但采用稍微不同的策略。

我不认为有一个预定义的协议处理程序可用于来自类路径的内容(所谓的classpath:协议)。

然而,Java允许您添加自己的协议。这是通过提供具体实现{{link2:java.net.URLStreamHandler}}和{{link3:java.net.URLConnection}}来完成的。

本文描述了如何实现自定义流处理程序:{{link4:http://java.sun.com/developer/onlineTraining/protocolhandlers/}}


4
你知道有哪些协议是随 JVM 一起提供的吗? - Thilo

5

我创建了一个类,帮助减少设置自定义处理程序时出现的错误,并利用系统属性,因此不会存在首先调用方法或不在正确容器中的问题。如果您出错了,还有一个异常类:

CustomURLScheme.java:
/*
 * The CustomURLScheme class has a static method for adding cutom protocol
 * handlers without getting bogged down with other class loaders and having to
 * call setURLStreamHandlerFactory before the next guy...
 */
package com.cybernostics.lib.net.customurl;

import java.net.URLStreamHandler;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Allows you to add your own URL handler without running into problems
 * of race conditions with setURLStream handler.
 * 
 * To add your custom protocol eg myprot://blahblah:
 * 
 * 1) Create a new protocol package which ends in myprot eg com.myfirm.protocols.myprot
 * 2) Create a subclass of URLStreamHandler called Handler in this package
 * 3) Before you use the protocol, call CustomURLScheme.add(com.myfirm.protocols.myprot.Handler.class);
 * @author jasonw
 */
public class CustomURLScheme
{

    // this is the package name required to implelent a Handler class
    private static Pattern packagePattern = Pattern.compile( "(.+\\.protocols)\\.[^\\.]+" );

    /**
     * Call this method with your handlerclass
     * @param handlerClass
     * @throws Exception 
     */
    public static void add( Class<? extends URLStreamHandler> handlerClass ) throws Exception
    {
        if ( handlerClass.getSimpleName().equals( "Handler" ) )
        {
            String pkgName = handlerClass.getPackage().getName();
            Matcher m = packagePattern.matcher( pkgName );

            if ( m.matches() )
            {
                String protocolPackage = m.group( 1 );
                add( protocolPackage );
            }
            else
            {
                throw new CustomURLHandlerException( "Your Handler class package must end in 'protocols.yourprotocolname' eg com.somefirm.blah.protocols.yourprotocol" );
            }

        }
        else
        {
            throw new CustomURLHandlerException( "Your handler class must be called 'Handler'" );
        }
    }

    private static void add( String handlerPackage )
    {
        // this property controls where java looks for
        // stream handlers - always uses current value.
        final String key = "java.protocol.handler.pkgs";

        String newValue = handlerPackage;
        if ( System.getProperty( key ) != null )
        {
            final String previousValue = System.getProperty( key );
            newValue += "|" + previousValue;
        }
        System.setProperty( key, newValue );
    }
}


CustomURLHandlerException.java:
/*
 * Exception if you get things mixed up creating a custom url protocol
 */
package com.cybernostics.lib.net.customurl;

/**
 *
 * @author jasonw
 */
public class CustomURLHandlerException extends Exception
{

    public CustomURLHandlerException(String msg )
    {
        super( msg );
    }

}

5
受@Stephen https://dev59.com/tHRA5IYBdhLWcg3wsgFP#1769454http://docstore.mik.ua/orelly/java/exp/ch09_06.htm的启发, 要使用:
new URL("classpath:org/my/package/resource.extension").openConnection()

只需创建此类到sun.net.www.protocol.classpath包中,并在Oracle JVM实现中运行它,就可以像魔术般工作。

package sun.net.www.protocol.classpath;

import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;

public class Handler extends URLStreamHandler {

    @Override
    protected URLConnection openConnection(URL u) throws IOException {
        return Thread.currentThread().getContextClassLoader().getResource(u.getPath()).openConnection();
    }
}

如果您使用其他JVM实现,请设置系统属性java.protocol.handler.pkgs=sun.net.www.protocol
FYI: http://docs.oracle.com/javase/7/docs/api/java/net/URL.html#URL(java.lang.String,%20java.lang.String,%20int,%20java.lang.String)

4

当然,使用注册URLStreamHandlers的解决方案是最正确的,但有时候需要最简单的解决方案。因此,我使用以下方法:

/**
 * Opens a local file or remote resource represented by given path.
 * Supports protocols:
 * <ul>
 * <li>"file": file:///path/to/file/in/filesystem</li>
 * <li>"http" or "https": http://host/path/to/resource - gzipped resources are supported also</li>
 * <li>"classpath": classpath:path/to/resource</li>
 * </ul>
 *
 * @param path An URI-formatted path that points to resource to be loaded
 * @return Appropriate implementation of {@link InputStream}
 * @throws IOException in any case is stream cannot be opened
 */
public static InputStream getInputStreamFromPath(String path) throws IOException {
    InputStream is;
    String protocol = path.replaceFirst("^(\\w+):.+$", "$1").toLowerCase();
    switch (protocol) {
        case "http":
        case "https":
            HttpURLConnection connection = (HttpURLConnection) new URL(path).openConnection();
            int code = connection.getResponseCode();
            if (code >= 400) throw new IOException("Server returned error code #" + code);
            is = connection.getInputStream();
            String contentEncoding = connection.getContentEncoding();
            if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip"))
                is = new GZIPInputStream(is);
            break;
        case "file":
            is = new URL(path).openStream();
            break;
        case "classpath":
            is = Thread.currentThread().getContextClassLoader().getResourceAsStream(path.replaceFirst("^\\w+:", ""));
            break;
        default:
            throw new IOException("Missed or unsupported protocol in path '" + path + "'");
    }
    return is;
}

3

我不确定是否已经有这样一个工具,但你可以很轻松地自己制作。

对于那个不同协议的示例,它看起来像是一个facade模式。当有不同的实现时,你会有一个共同的接口。

你可以使用相同的原则,创建一个ResourceLoader类,从你的属性文件中获取字符串,并检查我们的自定义协议。

myprotocol:a.xml
myprotocol:file:///tmp.txt
myprotocol:http://127.0.0.1:8080/a.properties
myprotocol:jar:http://www.foo.com/bar/baz.jar!/COM/foo/Quux.class

该函数会从字符串开头删除myprotocol:,然后根据加载资源的方式做出决策,并将资源返回给您。


如果您想使用第三方库来处理URL并且可能需要处理特定协议的资源解析,则此方法无法正常工作。 - mP.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接