URL per caricare risorse dal percorso di classe in Java


197

In Java, è possibile caricare tutti i tipi di risorse utilizzando la stessa API ma con protocolli URL diversi:

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

Ciò disaccoppia bene il caricamento effettivo della risorsa dall'applicazione che necessita della risorsa e, poiché un URL è solo una stringa, anche il caricamento delle risorse è facilmente configurabile.

Esiste un protocollo per caricare risorse usando l'attuale classloader? Questo è simile al protocollo Jar, tranne per il fatto che non ho bisogno di sapere da quale file jar o cartella di classe proviene la risorsa.

Posso farlo usando Class.getResourceAsStream("a.xml"), ovviamente, ma ciò mi richiederebbe di utilizzare un'API diversa e quindi modifiche al codice esistente. Voglio poterlo utilizzare in tutti i posti in cui posso già specificare un URL per la risorsa, semplicemente aggiornando un file delle proprietà.

Risposte:


348

Introduzione e implementazione di base

Prima di tutto, avrai bisogno almeno di un URLStreamHandler. Questo aprirà effettivamente la connessione a un determinato URL. Si noti che questo è semplicemente chiamato Handler; ciò consente di specificare java -Djava.protocol.handler.pkgs=org.my.protocolse verrà automaticamente prelevato, utilizzando il nome del pacchetto "semplice" come protocollo supportato (in questo caso "percorso di classe").

uso

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

Codice

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

Problemi di lancio

Se sei qualcosa come me, non vuoi fare affidamento su una proprietà impostata nel lancio per portarti da qualche parte (nel mio caso, mi piace tenere aperte le mie opzioni come Java WebStart - motivo per cui ho bisogno di tutto questo ).

Soluzioni temporanee / Miglioramenti

Codice manuale Specifiche del gestore

Se controlli il codice, puoi farlo

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

e questo utilizzerà il gestore per aprire la connessione.

Ma ancora una volta, questo è meno che soddisfacente, poiché non hai bisogno di un URL per farlo - vuoi farlo perché un po 'di lib che non puoi (o non vuoi) controllare vogliono gli URL ...

Registrazione gestore JVM

L'ultima opzione è quella di registrare un URLStreamHandlerFactoryche gestirà tutti gli URL in 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);
    }
}

Per registrare il gestore, chiamare URL.setURLStreamHandlerFactory()con il produttore configurato. Quindi fai new URL("classpath:org/my/package/resource.extension")come il primo esempio e via.

Problema di registrazione del gestore JVM

Si noti che questo metodo può essere chiamato solo una volta per JVM e si noti che Tomcat utilizzerà questo metodo per registrare un gestore JNDI (AFAIK). Prova Jetty (lo sarò); nel peggiore dei casi, è possibile utilizzare prima il metodo e poi deve aggirare intorno a te!

Licenza

Lo pubblico di pubblico dominio e chiedo che, se si desidera modificare, si avvia un progetto OSS da qualche parte e si commenta qui con i dettagli. Un'implementazione migliore sarebbe URLStreamHandlerFactoryquella di utilizzare una ThreadLocals per memorizzare URLStreamHandlers per ciascuna Thread.currentThread().getContextClassLoader(). Ti darò anche le mie modifiche e le lezioni di prova.


1
@Stephen questo è esattamente quello che sto cercando. Puoi per favore condividere i tuoi aggiornamenti con me? Posso includere come parte del mio com.github.fommil.common-utilspacchetto che prevedo di aggiornare e rilasciare presto tramite Sonatype.
fommil

5
Nota che puoi anche usare System.setProperty()per registrare il protocollo. Mi piaceSystem.setProperty("java.protocol.handler.pkgs", "org.my.protocols");
tsauerwein,

Java 9+ ha un metodo più semplice: stackoverflow.com/a/56088592/511976
mhvelplund

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

Questo dovrebbe farlo.


11
"Posso farlo utilizzando Class.getResourceAsStream (" a.xml "), ovviamente, ma ciò mi richiederebbe di utilizzare un'API diversa e quindi modifiche al codice esistente. Voglio poterlo utilizzare in tutti i luoghi in cui Posso già specificare un URL per la risorsa, semplicemente aggiornando un file delle proprietà. "
Thilo,

3
-1 Come sottolineato da Thilo, questo è qualcosa che l'OP ha considerato e respinto.
sleske,

13
getResource e getResourceAsStream sono metodi diversi. Concordato che getResourceAsStream non si adatta all'API, ma getResource restituisce un URL, che è esattamente ciò che OP ha richiesto.
Romacafe,

@romacafe: Sì, hai ragione. Questa è una buona soluzione alternativa.
sleske,

2
OP ha chiesto la soluzione del file delle proprietà, ma altri vengono anche qui a causa del titolo della domanda. E a loro piace questa soluzione dinamica :)
Jarekczek,

14

Penso che valga la sua risposta: se stai usando Spring, hai già questo

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

Come spiegato nella documentazione di primavera e sottolineato nei commenti di Skaffman.


La primavera IMHO ResourceLoader.getResource()è più appropriata per il compito ( ApplicationContext.getResource()delegati ad esso sotto il cofano)
Lu55,

11

È inoltre possibile impostare la proprietà a livello di codice durante l'avvio:

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

Utilizzando questa classe:

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

In questo modo ottieni il modo meno invadente per farlo. :) java.net.URL utilizzerà sempre il valore corrente dalle proprietà del sistema.


1
Il codice che aggiunge un pacchetto aggiuntivo per la ricerca alla java.protocol.handler.pkgsvariabile di sistema può essere utilizzato solo se il gestore mirava a elaborare un protocollo non ancora "noto" come gopher://. Se l'intenzione è quella di sovrascrivere il protocollo "popolare", come file://o http://, potrebbe essere troppo tardi per farlo, poiché alla java.net.URL#handlersmappa è già stato aggiunto un gestore "standard" per quel protocollo. Quindi l'unica via d'uscita è passare questa variabile a JVM.
dma_k,

6

(Simile alla risposta di Azder , ma un tocco leggermente diverso.)

Non credo che esista un gestore di protocollo predefinito per il contenuto del percorso di classe. (Il cosiddetto classpath:protocollo).

Tuttavia, Java ti consente di aggiungere i tuoi protocolli. Questo viene fatto fornendo implementazioni concrete java.net.URLStreamHandlere java.net.URLConnection.

Questo articolo descrive come implementare un gestore di flussi personalizzato: http://java.sun.com/developer/onlineTraining/protocolhandlers/ .


4
Conosci un elenco di quali protocolli vengono forniti con la JVM?
Thilo,

5

Ho creato una classe che aiuta a ridurre gli errori nell'impostazione dei gestori personalizzati e sfrutta la proprietà di sistema in modo che non ci siano problemi con la chiamata di un metodo prima o non essere nel contenitore giusto. C'è anche una classe di eccezione se sbagli le cose:

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

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

Usare

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

basta creare questa classe nel sun.net.www.protocol.classpathpacchetto ed eseguirla nell'implementazione Oracle JVM per funzionare come un incantesimo.

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

Nel caso in cui si stia utilizzando un'altra implementazione JVM, impostare la java.protocol.handler.pkgs=sun.net.www.protocolproprietà di sistema.

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


3

La soluzione con la registrazione di URLStreamHandlers è ovviamente più corretta, ma a volte è necessaria la soluzione più semplice. Quindi, uso il seguente metodo per quello:

/**
 * 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

Da Java 9+ in poi, puoi definirne uno nuovo URLStreamHandlerProvider. La URLclasse utilizza il framework del caricatore di servizi per caricarlo in fase di esecuzione.

Crea un provider:

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

}

Crea un file chiamato java.net.spi.URLStreamHandlerProvidernella META-INF/servicesdirectory con il contenuto:

org.example.ClasspathURLStreamHandlerProvider

Ora la classe URL utilizzerà il provider quando vede qualcosa di simile:

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

2

Non so se ce n'è già uno, ma puoi farlo da solo.

Quell'esempio di protocolli diversi mi sembra un modello di facciata. Hai un'interfaccia comune quando ci sono implementazioni diverse per ogni caso.

È possibile utilizzare lo stesso principio, creare una classe ResourceLoader che prende la stringa dal file delle proprietà e verifica la presenza di un nostro protocollo personalizzato

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

elimina il protocollo: dall'inizio della stringa e quindi decide in che modo caricare la risorsa e ti dà solo la risorsa.


Questo non funziona se si desidera che una libreria di terze parti utilizzi gli URL e che si desideri gestire la risoluzione delle risorse per un determinato protocollo.
mP.

2

Un'estensione alla risposta di Dilums :

Senza modificare il codice, probabilmente è necessario perseguire implementazioni personalizzate delle interfacce relative agli URL come raccomandato da Dilum. Per semplificarti le cose, posso consigliare di esaminare la fonte delle risorse di Spring Framework . Sebbene il codice non abbia la forma di un gestore di stream, è stato progettato per fare esattamente quello che stai cercando di fare ed è sotto la licenza ASL 2.0, rendendolo abbastanza facile da riutilizzare nel tuo codice con il dovuto credito.


La pagina a cui fai riferimento afferma che "non esiste un'implementazione URL standardizzata che può essere utilizzata per accedere a una risorsa che deve essere ottenuta dal percorso di classe o relativa a un ServletContext", che risponde alla mia domanda, immagino.
Thilo,

@Homeless: resisti, giovanotto. Con un po 'più di esperienza, pubblicherai presto commenti in pochissimo tempo.
Cuga,

1

In un'app Spring Boot, ho usato quanto segue per ottenere l'URL del file,

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

1

Se hai Tomcat sul percorso di classe, è semplice come:

TomcatURLStreamHandlerFactory.register();

Questo registrerà i gestori per i protocolli "war" e "classpath".


0

Cerco di evitare la URLlezione e invece mi affido URI. Quindi, per le cose che necessitano URLdove vorrei fare Spring Resource come la ricerca senza Spring, faccio quanto segue:

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

Per creare un URI puoi usare URI.create(..). In questo modo è anche meglio perché controlli quello ClassLoaderche farà la ricerca delle risorse.

Ho notato alcune altre risposte che cercavano di analizzare l'URL come una stringa per rilevare lo schema. Penso che sia meglio passare l'URI e usarlo per analizzare invece.

In realtà ho presentato un problema qualche tempo fa con Spring Source che implorava di separare il loro codice di risorse da in coremodo da non aver bisogno di tutte le altre cose di Spring.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.