URL для загрузки ресурсов из пути к классам в Java


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 для ресурса, просто обновив файл свойств.

Ответы:


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 для этого - вы хотите сделать это, потому что какая-то библиотека, которую вы не можете (или не хотите) контролировать, требует URL ...

Регистрация обработчика JVM

Окончательный вариант - зарегистрировать, URLStreamHandlerFactoryкоторый будет обрабатывать все URL через jvm:

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что использует ThreadLocals для хранения URLStreamHandlers для каждого Thread.currentThread().getContextClassLoader(). Я даже дам вам свои модификации и тестовые классы.


1
@ Стефен это именно то, что я ищу. Можете ли вы поделиться своими обновлениями со мной? Я могу включить в свой com.github.fommil.common-utilsпакет, который я планирую вскоре обновить и выпустить через Sonatype.
fommil

5
Обратите внимание, что вы также можете использовать System.setProperty()для регистрации протокола. ЛайкSystem.setProperty("java.protocol.handler.pkgs", "org.my.protocols");
цауэрвайн

В Java 9+ есть более простой метод: stackoverflow.com/a/56088592/511976
mhvelplund

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

Это должно сделать это.


11
«Я могу сделать это, используя Class.getResourceAsStream (« a.xml »), конечно, но это потребует от меня использования другого API и, следовательно, изменения в существующем коде. Я хочу иметь возможность использовать это во всех местах, где Я уже могу указать URL для ресурса, просто обновив файл свойств. "
Тило

3
-1 Как указал Тило, это то, что ОП рассмотрел и отверг.
слеске

13
getResource и getResourceAsStream - это разные методы. Согласился, что getResourceAsStream не соответствует API, но getResource возвращает URL-адрес, который является именно тем, что запрашивал OP.
romacafe

@romacafe: Да, ты прав. Это хорошее альтернативное решение.
Слеське

2
ОП попросил решение файла свойств, но другие также пришли сюда из-за названия вопроса. И им нравится это динамическое решение :)
Jarekczek

14

Я думаю, что это стоит своего ответа - если вы используете Spring, у вас уже есть это с

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

Как объяснено в весенней документации и указано в комментариях скаффмана.


ИМХО Весна ResourceLoader.getResource()больше подходит для этой задачи ( ApplicationContext.getResource()делегаты под нее под капотом)
Lu55

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

6

(Аналогично ответу Аздера , но немного другой такт.)

Я не верю, что есть предопределенный обработчик протокола для содержимого из classpath. (Так называемый classpath:протокол).

Однако Java позволяет вам добавлять свои собственные протоколы. Это делается путем предоставления конкретных реализаций java.net.URLStreamHandlerиjava.net.URLConnection .

В этой статье описывается, как может быть реализован пользовательский обработчик потока: http://java.sun.com/developer/onlineTraining/protocolhandlers/ .


4
Вы знаете список протоколов, поставляемых с JVM?
Тило

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/980442 и http://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 .String)


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+, вы можете определить новое 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.URLStreamHandlerProviderв META-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-адреса, и вы, возможно, хотите обработать разрешение ресурсов для определенного протокола.
мП

2

Расширение ответа Дилумса :

Без изменения кода вам, скорее всего, нужно следовать пользовательским реализациям интерфейсов, связанных с URL, как рекомендует Dilum. Чтобы упростить вам задачу, я могу порекомендовать посмотреть на источник ресурсов Spring Framework . Хотя код не в форме потокового обработчика, он был разработан для того, чтобы делать именно то, что вы хотите, и находится под лицензией ASL 2.0, что делает его достаточно дружественным для повторного использования в вашем коде с должным доверием.


На той странице, на которую вы ссылаетесь, говорится, что «не существует стандартизированной реализации URL, которая может использоваться для доступа к ресурсу, который должен быть получен из пути к классам, или относительно ServletContext», что, по-моему, отвечает на мой вопрос.
Тило

@Homeless: держись там, молодой человек. Приложив немного опыта, вы скоро будете публиковать комментарии в кратчайшие сроки.
Cuga

1

В приложении Spring Boot я использовал следующее, чтобы получить URL файла,

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

1

Если у вас есть tomcat на пути к классам, это так просто:

TomcatURLStreamHandlerFactory.register();

Это зарегистрирует обработчики для протоколов "war" и "classpath".


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, прося их отделить свой код ресурса от других ресурсов coreSpring.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.