URL zum Laden von Ressourcen aus dem Klassenpfad in Java

197

In Java können Sie alle Arten von Ressourcen mit derselben API, jedoch mit unterschiedlichen URL-Protokollen laden:

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

Dies entkoppelt das tatsächliche Laden der Ressource von der Anwendung, die die Ressource benötigt, und da eine URL nur eine Zeichenfolge ist, kann das Laden von Ressourcen auch sehr einfach konfiguriert werden.

Gibt es ein Protokoll zum Laden von Ressourcen mit dem aktuellen Klassenladeprogramm? Dies ähnelt dem Jar-Protokoll, außer dass ich nicht wissen muss, aus welcher JAR-Datei oder welchem ​​Klassenordner die Ressource stammt.

Ich kann das Class.getResourceAsStream("a.xml")natürlich mit verwenden, aber dafür müsste ich eine andere API verwenden und daher den vorhandenen Code ändern. Ich möchte dies an allen Stellen verwenden können, an denen ich bereits eine URL für die Ressource angeben kann, indem ich nur eine Eigenschaftendatei aktualisiere.

Thilo
quelle

Antworten:

348

Einführung und grundlegende Implementierung

Zunächst benötigen Sie mindestens einen URLStreamHandler. Dadurch wird die Verbindung zu einer bestimmten URL tatsächlich geöffnet. Beachten Sie, dass dies einfach aufgerufen wird Handler. Auf diese Weise können Sie angeben, dass java -Djava.protocol.handler.pkgs=org.my.protocolses automatisch unter Verwendung des "einfachen" Paketnamens als unterstütztes Protokoll (in diesem Fall "Klassenpfad") abgeholt wird.

Verwendung

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

Code

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

Probleme starten

Wenn Sie so etwas wie ich sind, möchten Sie sich nicht darauf verlassen, dass beim Start eine Eigenschaft festgelegt wird, um Sie irgendwohin zu bringen (in meinem Fall möchte ich meine Optionen wie Java WebStart offen halten - deshalb brauche ich all dies ).

Problemumgehungen / Verbesserungen

Manuelle Code-Handler-Spezifikation

Wenn Sie den Code steuern, können Sie dies tun

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

und dies wird Ihren Handler verwenden, um die Verbindung zu öffnen.

Aber auch dies ist weniger als zufriedenstellend, da Sie dazu keine URL benötigen - Sie möchten dies tun, weil eine Bibliothek, die Sie nicht steuern können (oder wollen), URLs benötigt ...

Registrierung des JVM-Handlers

Die ultimative Option besteht darin, eine zu registrieren URLStreamHandlerFactory, die alle URLs im gesamten JVM verarbeitet:

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

Um den Handler zu registrieren, wenden Sie sich URL.setURLStreamHandlerFactory()an Ihre konfigurierte Fabrik. Dann new URL("classpath:org/my/package/resource.extension")gefällt dir das erste Beispiel und los geht's.

Problem bei der Registrierung des JVM-Handlers

Beachten Sie, dass diese Methode nur einmal pro JVM aufgerufen werden darf, und beachten Sie, dass Tomcat diese Methode verwendet, um einen JNDI-Handler (AFAIK) zu registrieren. Versuchen Sie Jetty (ich werde sein); im schlimmsten Fall können Sie die Methode zuerst anwenden und dann muss sie um Sie herum funktionieren!

Lizenz

Ich gebe dies für die Öffentlichkeit frei und bitte Sie, wenn Sie Änderungen vornehmen möchten, irgendwo ein OSS-Projekt zu starten und hier mit den Details zu kommentieren. Eine bessere Implementierung wäre eine URLStreamHandlerFactory, die ThreadLocals verwendet, um URLStreamHandlers für jede zu speichern Thread.currentThread().getContextClassLoader(). Ich werde Ihnen sogar meine Modifikationen und Testklassen geben.

Stephen
quelle
1
@ Stephen das ist genau das, wonach ich suche. Können Sie mir bitte Ihre Updates mitteilen? Ich kann als Teil meines com.github.fommil.common-utilsPakets hinzufügen, dass ich in Kürze über Sonatype aktualisieren und veröffentlichen möchte.
Uhr
5
Beachten Sie, dass Sie System.setProperty()das Protokoll auch registrieren können. LikeSystem.setProperty("java.protocol.handler.pkgs", "org.my.protocols");
Tsauerwein
Java 9+ hat eine einfachere Methode: stackoverflow.com/a/56088592/511976
mhvelplund
100
URL url = getClass().getClassLoader().getResource("someresource.xxx");

Das sollte es tun.

Rasin
quelle
11
"Ich kann das natürlich mit Class.getResourceAsStream (" a.xml ") tun, aber dafür müsste ich eine andere API verwenden und daher den vorhandenen Code ändern. Ich möchte dies an allen Stellen verwenden können, an denen Ich kann bereits eine URL für die Ressource angeben, indem ich nur eine Eigenschaftendatei aktualisiere. "
Thilo
3
-1 Wie Thilo betonte, hat das OP dies geprüft und abgelehnt.
Sleske
13
getResource und getResourceAsStream sind verschiedene Methoden. Einverstanden, dass getResourceAsStream nicht zur API passt, getResource jedoch eine URL zurückgibt, nach der OP genau gefragt hat.
Romacafe
@romacafe: Ja, du hast recht. Dies ist eine gute alternative Lösung.
Sleske
2
OP hat nach einer Lösung für Eigenschaftendateien gefragt, aber andere kommen auch wegen des Titels der Frage hierher. Und sie mögen diese dynamische Lösung :)
Jarekczek
14

Ich denke, das ist eine eigene Antwort wert - wenn Sie Spring verwenden, haben Sie dies bereits mit

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

Wie in der Frühjahrsdokumentation erklärt und in den Kommentaren von skaffman hervorgehoben.

eis
quelle
IMHO Spring ResourceLoader.getResource()ist besser für die Aufgabe geeignet ( ApplicationContext.getResource()Delegierte unter der Haube)
Lu55
11

Sie können die Eigenschaft auch beim Start programmgesteuert festlegen:

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

Verwenden dieser Klasse:

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

Auf diese Weise erhalten Sie den am wenigsten aufdringlichen Weg, dies zu tun. :) java.net.URL verwendet immer den aktuellen Wert aus den Systemeigenschaften.

subes
quelle
1
Der Code, der der java.protocol.handler.pkgsSystemvariablen ein zusätzliches Paket für die Suche hinzufügt , kann nur verwendet werden, wenn der Handler darauf abzielt, noch nicht "bekannte" Protokolle wie z gopher://. Wenn beabsichtigt wird, "populäres" Protokoll wie file://oder zu überschreiben http://, könnte es zu spät sein, dies zu tun, da java.net.URL#handlersmap bereits ein "Standard" -Handler für dieses Protokoll hinzugefügt wurde. Der einzige Ausweg besteht darin, diese Variable an JVM zu übergeben.
dma_k
6

(Ähnlich wie Azder's Antwort , aber ein etwas anderer Takt.)

Ich glaube nicht, dass es einen vordefinierten Protokollhandler für Inhalte aus dem Klassenpfad gibt. (Das sogenannte classpath:Protokoll).

Mit Java können Sie jedoch Ihre eigenen Protokolle hinzufügen. Dies geschieht durch die Bereitstellung konkreter Implementierungen java.net.URLStreamHandlerund java.net.URLConnection.

Dieser Artikel beschreibt, wie ein benutzerdefinierter Stream-Handler implementiert werden kann: http://java.sun.com/developer/onlineTraining/protocolhandlers/ .

Dilum Ranatunga
quelle
4
Kennen Sie eine Liste der Protokolle, die mit der JVM geliefert werden?
Thilo
5

Ich habe eine Klasse erstellt, die dazu beiträgt, Fehler beim Einrichten benutzerdefinierter Handler zu reduzieren und die Systemeigenschaft zu nutzen, sodass es keine Probleme gibt, eine Methode zuerst aufzurufen oder sich nicht im richtigen Container zu befinden. Es gibt auch eine Ausnahmeklasse, wenn Sie etwas falsch machen:

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
quelle
5

Inspirieren Sie von @Stephen https://stackoverflow.com/a/1769454/980442 und http://docstore.mik.ua/orelly/java/exp/ch09_06.htm

Benutzen

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

Erstellen Sie diese Klasse einfach in einem sun.net.www.protocol.classpathPaket und führen Sie sie in der Oracle JVM-Implementierung aus, damit sie wie ein Zauber funktioniert.

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

Wenn Sie eine andere JVM-Implementierung verwenden, stellen Sie die java.protocol.handler.pkgs=sun.net.www.protocol Systemeigenschaft fest.

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

Daniel De León
quelle
3

Die Lösung mit der Registrierung von URLStreamHandlers ist natürlich am korrektesten, aber manchmal wird die einfachste Lösung benötigt. Also benutze ich dafür die folgende Methode:

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

Ab Java 9+ können Sie eine neue definieren URLStreamHandlerProvider. Die URLKlasse verwendet das Service Loader-Framework, um es zur Laufzeit zu laden.

Erstellen Sie einen Anbieter:

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

}

Erstellen Sie eine java.net.spi.URLStreamHandlerProviderim META-INF/servicesVerzeichnis aufgerufene Datei mit dem Inhalt:

org.example.ClasspathURLStreamHandlerProvider

Jetzt verwendet die URL-Klasse den Anbieter, wenn sie Folgendes sieht:

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

Ich weiß nicht, ob es bereits eine gibt, aber Sie können es einfach selbst machen.

Dieses Beispiel für verschiedene Protokolle sieht für mich wie ein Fassadenmuster aus. Sie haben eine gemeinsame Schnittstelle, wenn es für jeden Fall unterschiedliche Implementierungen gibt.

Sie können dasselbe Prinzip verwenden, eine ResourceLoader-Klasse erstellen, die die Zeichenfolge aus Ihrer Eigenschaftendatei übernimmt, und nach einem benutzerdefinierten Protokoll von uns suchen

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

entfernt das myprotocol: vom Anfang der Zeichenfolge und entscheidet dann, wie die Ressource geladen werden soll, und gibt Ihnen nur die Ressource.

Azder
quelle
Dies funktioniert nicht, wenn eine Drittanbieter-Bibliothek URLs verwendet und Sie möglicherweise die Auflösung von Ressourcen für ein bestimmtes Protokoll übernehmen möchten.
mP.
2

Eine Erweiterung von Dilums 'Antwort :

Ohne Code zu ändern, müssen Sie wahrscheinlich benutzerdefinierte Implementierungen von URL-bezogenen Schnittstellen durchführen, wie von Dilum empfohlen. Um Ihnen die Arbeit zu vereinfachen, kann ich empfehlen, in der Quelle nach den Ressourcen von Spring Framework zu suchen . Obwohl der Code nicht in Form eines Stream-Handlers vorliegt, wurde er so konzipiert, dass er genau das tut, was Sie möchten. Er steht unter der ASL 2.0-Lizenz und ist daher so benutzerfreundlich, dass er in Ihrem Code mit angemessener Gutschrift wiederverwendet werden kann.

DavidValeri
quelle
Auf dieser Seite, auf die Sie verweisen, heißt es: "Es gibt keine standardisierte URL-Implementierung, die verwendet werden kann, um auf eine Ressource zuzugreifen, die über den Klassenpfad oder relativ zu einem ServletContext abgerufen werden muss."
Thilo
@ Homeless: Bleib dran, junger Mann. Mit etwas mehr Erfahrung werden Sie in Kürze Kommentare veröffentlichen.
Cuga
1

In einer Spring Boot-App habe ich Folgendes verwendet, um die Datei-URL abzurufen:

Thread.currentThread().getContextClassLoader().getResource("PromotionalOfferIdServiceV2.wsdl")
Sathesh
quelle
1

Wenn Sie Tomcat auf dem Klassenpfad haben, ist es so einfach wie:

TomcatURLStreamHandlerFactory.register();

Dadurch werden Handler für die Protokolle "Krieg" und "Klassenpfad" registriert.

Olivier Gérardin
quelle
0

Ich versuche, die URLKlasse zu meiden und verlasse mich stattdessen auf URI. Daher URLmache ich für Dinge, die benötigt werden, wo ich Spring Resource ausführen möchte, wie das Nachschlagen ohne Spring, Folgendes:

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

Um einen URI zu erstellen, können Sie verwenden URI.create(..). Dieser Weg ist auch besser, weil Sie steuern ClassLoader, wer die Ressourcensuche durchführen soll.

Ich bemerkte einige andere Antworten, die versuchten, die URL als Zeichenfolge zu analysieren, um das Schema zu erkennen. Ich denke, es ist besser, URI weiterzugeben und stattdessen zum Parsen zu verwenden.

Ich habe vor einiger Zeit ein Problem bei Spring Source eingereicht und sie gebeten, ihren Ressourcencode von corediesem zu trennen, damit Sie nicht alle anderen Spring-Inhalte benötigen.

Adam Gent
quelle