Adres URL do ładowania zasobów ze ścieżki klas w Javie

197

W Javie można ładować wszelkiego rodzaju zasoby przy użyciu tego samego interfejsu API, ale z różnymi protokołami URL:

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

To ładnie oddziela faktyczne ładowanie zasobu od aplikacji, która potrzebuje zasobu, a ponieważ URL jest tylko ciągiem, ładowanie zasobów jest również bardzo łatwe do skonfigurowania.

Czy istnieje protokół ładowania zasobów za pomocą bieżącego modułu ładującego klasy? Jest to podobne do protokołu Jar, z tym wyjątkiem, że nie muszę wiedzieć, z którego pliku jar lub folderu klasy pochodzi zasób.

Oczywiście mogę to zrobić Class.getResourceAsStream("a.xml"), ale wymagałoby to użycia innego interfejsu API, a tym samym zmian w istniejącym kodzie. Chcę mieć możliwość korzystania z tego we wszystkich miejscach, w których mogę już podać adres URL zasobu, po prostu aktualizując plik właściwości.

Thilo
źródło

Odpowiedzi:

348

Wprowadzenie i podstawowe wdrożenie

Po pierwsze, potrzebujesz przynajmniej URLStreamHandler. To faktycznie otworzy połączenie z danym adresem URL. Zauważ, że jest to po prostu nazywane Handler; pozwala to określić java -Djava.protocol.handler.pkgs=org.my.protocolsi zostanie on automatycznie pobrany, używając „prostej” nazwy pakietu jako obsługiwanego protokołu (w tym przypadku „ścieżka klasy”).

Stosowanie

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

Kod

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

Uruchom problemy

Jeśli masz coś takiego jak ja, nie chcesz polegać na nieruchomości będącej w zestawie z uruchomieniem, aby dostać się gdzieś (w moim przypadku, jak zachować moje opcje otworzyć jak Java Web Start - dlatego ja potrzebuję to wszystko ).

Obejścia / ulepszenia

Instrukcja obsługi kodu obsługi

Jeśli kontrolujesz kod, możesz to zrobić

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

a to użyje twojego programu obsługi do otwarcia połączenia.

Ale znowu, to jest mniej niż zadowalające, ponieważ nie potrzebujesz do tego adresu URL - chcesz to zrobić, ponieważ niektóre biblioteki, których nie możesz kontrolować (lub nie chcesz) kontrolować, chcą adresów URL ...

Rejestracja JVM Handler

Ostateczną opcją jest zarejestrowanie pliku, URLStreamHandlerFactoryktóry będzie obsługiwał wszystkie adresy URL w 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);
    }
}

Aby zarejestrować program obsługi, zadzwoń URL.setURLStreamHandlerFactory()do skonfigurowanej fabryki. Zrób new URL("classpath:org/my/package/resource.extension")więc pierwszy przykład i już nie ma go.

Problem z rejestracją modułu obsługi JVM

Należy pamiętać, że tę metodę można wywołać tylko raz dla JVM, i należy pamiętać, że Tomcat użyje tej metody do zarejestrowania procedury obsługi JNDI (AFAIK). Spróbuj Jetty (będę); w najgorszym wypadku możesz najpierw użyć tej metody, a potem musi ona obejść cię!

Licencja

Udostępniam to do domeny publicznej i proszę, jeśli chcesz zmodyfikować, rozpocznij gdzieś projekt OSS i skomentuj tutaj ze szczegółami. Lepszą implementacją byłoby posiadanie s URLStreamHandlerFactoryużywającego ThreadLocals do przechowywania URLStreamHandlers dla każdego Thread.currentThread().getContextClassLoader(). Dam ci nawet moje modyfikacje i klasy testowe.

Stephen
źródło
1
@Stephen właśnie tego szukam. Czy możesz udostępnić mi swoje aktualizacje? Mogę dołączyć jako część mojego com.github.fommil.common-utilspakietu, który planuję wkrótce zaktualizować i wydać za pośrednictwem Sonatype.
fommil
5
Pamiętaj, że możesz także użyć System.setProperty()do zarejestrowania protokołu. LikeSystem.setProperty("java.protocol.handler.pkgs", "org.my.protocols");
tsauerwein
Java 9+ ma prostszą metodę: stackoverflow.com/a/56088592/511976
mhvelplund
100
URL url = getClass().getClassLoader().getResource("someresource.xxx");

Że należy to zrobić.

Rasin
źródło
11
„Mogę to zrobić za pomocą Class.getResourceAsStream („ a.xml ”), oczywiście, ale wymagałoby to użycia innego interfejsu API, a zatem zmian w istniejącym kodzie. Chcę mieć możliwość korzystania z tego we wszystkich miejscach, w których Mogę już podać adres URL zasobu, po prostu aktualizując plik właściwości. ”
Thilo,
3
-1 Jak zauważył Thilo, jest to coś, co OP rozpatrzył i odrzucił.
śleske,
13
getResource i getResourceAsStream to różne metody. Uzgodniono, że getResourceAsStream nie pasuje do API, ale getResource zwraca adres URL, dokładnie o to poprosił OP.
romacafe,
@romacafe: Tak, masz rację. To dobre alternatywne rozwiązanie.
sleske,
2
OP poprosił o rozwiązanie pliku właściwości, ale inni też tu przychodzą ze względu na tytuł pytania. I podoba im się to dynamiczne rozwiązanie :)
Jarekczek
14

Myślę, że jest to warte własnej odpowiedzi - jeśli używasz Wiosny, już to masz

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

Jak wyjaśniono w wiosennej dokumentacji i wskazano w komentarzach skaffmana.

eis
źródło
IMHO Wiosna ResourceLoader.getResource()jest bardziej odpowiednia do zadania ( ApplicationContext.getResource()delegaci do niego pod maską)
Lu55
11

Możesz także ustawić właściwość programowo podczas uruchamiania:

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

Korzystanie z tej klasy:

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

W ten sposób otrzymujesz najmniej inwazyjny sposób na zrobienie tego. :) java.net.URL zawsze użyje bieżącej wartości z właściwości systemowych.

sub
źródło
1
Kod, który dodaje dodatkowy pakiet do wyszukiwania do java.protocol.handler.pkgszmiennej systemowej, może być użyty tylko wtedy, gdy program obsługi zamierzał przetworzyć jeszcze „nieznany” protokół, taki jak gopher://. Jeśli celem jest zastąpienie „popularnego” protokołu, takiego jak file://lub http://, może być za późno, aby to zrobić, ponieważ java.net.URL#handlersmapa została już dodana „standardowa” procedura obsługi tego protokołu. Jedynym wyjściem jest przekazanie tej zmiennej do JVM.
dma_k,
6

(Podobne do odpowiedzi Azdera , ale nieco inny takt.)

Nie sądzę, aby istniała predefiniowana procedura obsługi protokołu dla treści z ścieżki klasy. (Tak zwany classpath:protokół).

Jednak Java pozwala dodawać własne protokoły. Odbywa się to poprzez zapewnienie konkretnych wdrożeń java.net.URLStreamHandleri java.net.URLConnection.

W tym artykule opisano, jak można zaimplementować niestandardową procedurę obsługi strumienia: http://java.sun.com/developer/onlineTraining/protocolhandlers/ .

Dilum Ranatunga
źródło
4
Czy znasz listę protokołów dostarczanych z JVM?
Thilo
5

Stworzyłem klasę, która pomaga zmniejszyć liczbę błędów podczas konfigurowania niestandardowych procedur obsługi i korzysta z właściwości systemowej, dzięki czemu nie ma problemów z pierwszym wywołaniem metody lub brakiem odpowiedniego kontenera. Istnieje również klasa wyjątków, jeśli coś jest nie tak:

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

}
Jason Wraxall
źródło
5

Inspiruj @Stephen https://stackoverflow.com/a/1769454/980442 i http://docstore.mik.ua/orelly/java/exp/ch09_06.htm

Używać

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

po prostu utwórz tę klasę w sun.net.www.protocol.classpathpakiecie i uruchom ją w implementacji Oracle JVM, aby działała jak urok.

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

Jeśli używasz innej implementacji JVM, ustaw java.protocol.handler.pkgs=sun.net.www.protocolwłaściwość systemową.

FYI: http://docs.oracle.com/javase/7/docs/api/java/net/URL.html#URL(java.lang.String,%20java.lang.String,%20int,%20java.lang .Strunowy)

Daniel De León
źródło
3

Rozwiązanie z rejestracją URLStreamHandlers jest oczywiście najbardziej poprawne, ale czasami potrzebne jest najprostsze rozwiązanie. Dlatego używam do tego następującej metody:

/**
 * 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;
}
domax
źródło
3

Począwszy od wersji Java 9+, możesz zdefiniować nowy URLStreamHandlerProvider. URLKlasa wykorzystuje ramy ładowarka usług załadować go w czasie wykonywania.

Utwórz dostawcę:

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

}

Utwórz plik o nazwie java.net.spi.URLStreamHandlerProviderw META-INF/serviceskatalogu z zawartością:

org.example.ClasspathURLStreamHandlerProvider

Teraz klasa URL użyje dostawcy, gdy zobaczy coś takiego:

URL url = new URL("classpath:myfile.txt");
mhvelplund
źródło
2

Nie wiem, czy już istnieje, ale możesz to zrobić samemu.

Ten przykład różnych protokołów wygląda dla mnie jak wzór elewacji. Masz wspólny interfejs, gdy dla każdej sprawy istnieją różne implementacje.

Możesz użyć tej samej zasady, stworzyć klasę ResourceLoader, która pobiera ciąg z pliku właściwości i sprawdza nasz niestandardowy protokół

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

usuwa myprotocol: od początku łańcucha, a następnie decyduje, w jaki sposób załadować zasób, i po prostu daje ci zasób.

Azder
źródło
To nie działa, jeśli chcesz, aby biblioteka zewnętrzna korzystała z adresów URL i być może chcesz poradzić sobie z rozdzielczością zasobów dla określonego protokołu.
mP.
2

Rozszerzenie odpowiedzi Dilums :

Bez zmiany kodu prawdopodobnie będziesz potrzebować niestandardowych implementacji interfejsów związanych z adresami URL, jak zaleca Dilum. Aby uprościć ci wszystko, mogę polecić przyjrzenie się źródłu zasobów Spring Framework . Chociaż kod nie ma postaci procedury obsługi strumienia, został zaprojektowany do robienia dokładnie tego, co chcesz, i jest objęty licencją ASL 2.0, dzięki czemu jest wystarczająco przyjazny dla ponownego użycia w kodzie z należytym uznaniem.

DavidValeri
źródło
Strona, na którą się powołujesz, stwierdza, że ​​„nie ma znormalizowanej implementacji adresu URL, która mogłaby zostać wykorzystana do uzyskania dostępu do zasobu, który należy uzyskać ze ścieżki klas lub względem ServletContext”, który, jak sądzę, odpowiada na moje pytanie.
Thilo
@Homeless: trzymaj się, młody człowieku. Przy odrobinie doświadczenia wkrótce opublikujesz komentarze w krótkim czasie.
Cuga
1

W aplikacji Spring Boot użyłem następującego polecenia, aby uzyskać adres URL pliku,

Thread.currentThread().getContextClassLoader().getResource("PromotionalOfferIdServiceV2.wsdl")
Sathesh
źródło
1

Jeśli masz tomcat na ścieżce klasy, jest to tak proste, jak:

TomcatURLStreamHandlerFactory.register();

Spowoduje to zarejestrowanie procedur obsługi protokołów „war” i „classpath”.

Olivier Gérardin
źródło
0

Staram się unikać URLzajęć i polegam na nich URI. Tak więc dla rzeczy, które potrzebują miejsca, w URLktórym chciałbym wykonać Spring Resource, takich jak wyszukiwanie bez Spring, wykonuję następujące czynności:

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

Aby utworzyć identyfikator URI, możesz użyć URI.create(..). Ten sposób jest również lepszy, ponieważ kontrolujesz, ClassLoaderktóry będzie wyszukiwał zasoby.

Zauważyłem kilka innych odpowiedzi próbujących parsować adres URL jako ciąg znaków w celu wykrycia schematu. Myślę, że lepiej jest przekazać URI i użyć go do analizy.

Właściwie już jakiś czas temu zgłosiłem problem z Spring Source, prosząc ich o oddzielenie kodu zasobów core, abyś nie potrzebował wszystkich innych rzeczy z Springa.

Adam Gent
źródło