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


197

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

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的所有地方使用它。

Answers:


348

介绍和基本实现

首先,您将至少需要一个URLStreamHandler。这实际上将打开与给定URL的连接。注意,这简称为Handler; 这样您就可以指定java -Djava.protocol.handler.pkgs=org.my.protocols它,并且它将使用“简单”程序包名称作为受支持的协议(在这种情况下为“ 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即可执行此操作-之所以要这样做是因为您无法(或不想控制)的某些lib想要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使用ThreadLocal来存储。我什至会给您我的修改和测试课程。URLStreamHandlerThread.currentThread().getContextClassLoader()


1
@Stephen这正是我要的东西。您能和我分享您的更新吗?我可以将com.github.fommil.common-utils计划通过Sonatype进行更新和发布的软件包作为软件包的一部分。
fommil

5
请注意,您也可以使用System.setProperty()注册协议。赞System.setProperty("java.protocol.handler.pkgs", "org.my.protocols");
tsauerwein 2014年

Java 9+有一个更简单的方法:stackoverflow.com/a/56088592/511976
mhvelplund

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

那应该做。


11
“当然,我可以使用Class.getResourceAsStream(“ a.xml”)来做到这一点,但这需要我使用不同的API,因此需要对现有代码进行更改。我希望能够在所有需要使用此代码的地方使用它我可以通过更新属性文件来为资源指定URL。”
Thilo

3
-1正如Thilo所指出的,这是OP所考虑并拒绝的。
sleske 2011年

13
getResource和getResourceAsStream是不同的方法。同意getResourceAsStream不适合该API,但是getResource返回一个URL,这正是OP要求的。
romacafe 2011年

@romacafe:是的,您是对的。这是一个很好的替代解决方案。
sleske 2011年

2
OP要求提供属性文件解决方案,但由于问题的标题,其他人也来了。他们喜欢这个动态的解决方案:)
Jarekczek '17

14

我认为这是值得回答的问题-如果您使用的是Spring,则已经有了

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

就像在春天文档中解释的那样,在skaffman的评论中指出。


恕我直言,Spring ResourceLoader.getResource()更适合该任务(ApplicationContext.getResource()
幕后为其

11

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

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将始终使用系统属性中的当前值。


1
java.protocol.handler.pkgs仅当处理程序旨在处理尚未使用的“未知”协议(例如)时,才可以使用向系统变量添加用于查找的额外程序包的代码gopher://。如果打算覆盖“” file://或“”这样的“流行”协议,http://那就太迟了,因为java.net.URL#handlersmap已经为该协议添加了“标准”处理程序。因此,唯一的出路是将该变量传递给JVM。
dma_k 2014年

6

(与Azder的回答类似,但略有不同。)

我认为对于类路径中的内容没有预定义的协议处理程序。(所谓classpath:协议)。

但是,Java确实允许您添加自己的协议。这是通过提供具体的实现java.net.URLStreamHandler和完成的java.net.URLConnection

本文介绍如何实现自定义流处理程序: 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://stackoverflow.com/a/1769454/980442http://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系统属性。

仅供参考:http : //docs.oracle.com/javase/7/docs/api/java/net/URL.html#URL( java.lang.String,%20java.lang.String,%20int,% 20java.lang 。串)


3

当然,注册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

从Java 9+开始,您可以定义一个new URLStreamHandlerProvider。本URL类使用的服务加载架构在运行时加载它。

创建一个提供程序:

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;
    }

}

java.net.spi.URLStreamHandlerProviderMETA-INF/services目录中创建一个包含以下内容的文件:

org.example.ClasspathURLStreamHandlerProvider

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

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

2

我不知道是否已经有一个,但是您可以自己设置。

在我看来,不同的协议示例看起来像是外观模式。在每种情况下都有不同的实现时,您将具有一个公共接口。

您可以使用相同的原理,创建一个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。

2

Dilums答案的扩展:

在不更改代码的情况下,您可能需要按照Dilum的建议追求URL相关接口的自定义实现。为了简化您的工作,我建议您查看Spring Framework的参考资料源。尽管代码不是流处理程序的形式,但它已被设计为完全按照您的意愿进行操作,并且已获得ASL 2.0许可,从而使其足够友好,可以在您的代码中以应有的信誉再次使用。


您所引用的页面指出“没有标准化的URL实现可用于访问需要从类路径或相对于ServletContext获取的资源”,我想这回答了我的问题。
Thilo

@无家可归:年轻人,挂在那里。有了更多的经验,您很快就会很快发表评论。
卡加

1

在Spring Boot应用程序中,我使用以下命令获取文件URL,

Thread.currentThread().getContextClassLoader().getResource("PromotionalOfferIdServiceV2.wsdl")


0

我尝试避免URL上课,而是依靠URI。因此,对于需要URL在我想做Spring Resource的地方做的事情,例如不使用Spring进行查找,我可以执行以下操作:

public static URL toURL(URI u, ClassLoader loader) throws MalformedURLException {
    if ("classpath".equals(u.getScheme())) {
        String path = u.getPath();
        if (path.startsWith("/")){
            path = path.substring("/".length());
        }
        return loader.getResource(path);
    }
    else if (u.getScheme() == null && u.getPath() != null) {
        //Assume that its a file.
        return new File(u.getPath()).toURI().toURL();
    }
    else {
        return u.toURL();
    }
}

要创建URI,可以使用URI.create(..)。这种方法也更好,因为您控制ClassLoader将执行资源查找的。

我注意到其他一些尝试将URL解析为字符串以检测方案的答案。我认为最好绕过URI并使用它来解析。

实际上,我之前已经提交了一个问题,Spring Source恳求他们从中分离出Resource代码,core这样您就不需要所有其他Spring东西了。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.