URL для завантаження ресурсів з classpath на 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-адреса - ви хочете це зробити, тому що якась lib, яку ви не можете (або не хочете) контролювати, хоче URL-адреси ...

Реєстрація JVM Handler

Кінцева опція - це зареєструвати a, 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
@Stephen це саме те, що я шукаю. Чи можете ви поділитися своїми оновленнями зі мною? Я можу включити як частину свого com.github.fommil.common-utilsпакету, який я планую найближчим часом оновити та випустити через Sonatype.
fommil

5
Зауважте, що ви також можете використовувати System.setProperty()для реєстрації протоколу. ЯкSystem.setProperty("java.protocol.handler.pkgs", "org.my.protocols");
tsauerwein

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 Як зазначив Тило, це те, що ОП розглядало та відкидало.
sleske

13
getResource та getResourceAsStream - це різні методи. Погодились, що getResourceAsStream не відповідає API, але getResource повертає URL-адресу, що саме те, про що вимагала ОП.
romacafe

@romacafe: Так, ти маєш рацію. Це хороше альтернативне рішення.
sleske

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#handlersкарта вже додала "стандартний" обробник для цього протоколу. Тож єдиний вихід - передати цю змінну 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властивість системи.

FYI: 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+ і вище можна визначити нове 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: з початку рядка, а потім приймає рішення, яким способом завантажувати ресурс, і просто дає вам ресурс.


Це не спрацьовує, якщо ви хочете, щоб сторона lib використовувала URL-адреси, і, можливо, ви хочете обробляти роздільну здатність ресурсів для певного протоколу.
мП.

2

Розширення до відповіді Ділумса :

Не змінюючи код, вам, швидше за все, потрібно буде використовувати власні реалізації інтерфейсів, пов’язаних із URL-адресами, як рекомендує Dilum. Щоб спростити речі для вас, я можу порекомендувати переглянути джерело ресурсів Spring Framework . Хоча код не має форми обробника потоку, він був розроблений для того, щоб робити саме те, що ви прагнете, і під ліцензією ASL 2.0, що робить його досить дружним для повторного використання у вашому коді з належним кредитом.


На цій сторінці, на яку ви посилаєтесь, вказується, що "не існує стандартизованої реалізації URL-адреси, яка могла б бути використана для доступу до ресурсу, який потрібно отримати з класового шляху або відносно ServletContext", який, напевно, відповідає на моє запитання.
Тіло

@Homeless: туди, молодий чоловік. Маючи трохи більше досвіду, ви незабаром опублікуєте коментарі в найкоротші терміни.
Куга

1

У додатку Spring Boot я використовував таке, щоб отримати URL-адресу файлу,

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

1

Якщо у вас є tomcat на classpath, це так просто, як:

TomcatURLStreamHandlerFactory.register();

Це зареєструє обробники для протоколів "війни" та "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, благаючи їх відокремити свій код ресурсу, coreщоб вам не потрібні всі інші весняні речі.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.