URL pour charger des ressources à partir du chemin de classe en Java

197

En Java, vous pouvez charger toutes sortes de ressources en utilisant la même API mais avec différents protocoles URL:

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

Cela dissocie bien le chargement réel de la ressource de l'application qui a besoin de la ressource, et comme une URL n'est qu'une chaîne, le chargement de la ressource est également très facilement configurable.

Existe-t-il un protocole pour charger les ressources à l'aide du chargeur de classe actuel? Ceci est similaire au protocole Jar, sauf que je n'ai pas besoin de savoir de quel fichier jar ou dossier de classe la ressource provient.

Je peux le faire en utilisant Class.getResourceAsStream("a.xml"), bien sûr, mais cela nécessiterait que j'utilise une API différente, et donc des modifications du code existant. Je veux pouvoir l'utiliser dans tous les endroits où je peux déjà spécifier une URL pour la ressource, simplement en mettant à jour un fichier de propriétés.

Thilo
la source

Réponses:

348

Introduction et implémentation de base

Tout d'abord, vous allez avoir besoin d'au moins un URLStreamHandler. Cela ouvrira en fait la connexion à une URL donnée. Notez que cela s'appelle simplement Handler; cela vous permet de spécifier java -Djava.protocol.handler.pkgs=org.my.protocolset il sera automatiquement récupéré, en utilisant le nom de package "simple" comme protocole pris en charge (dans ce cas "chemin de classe").

Usage

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

Problèmes de lancement

Si vous êtes comme moi, vous ne voulez pas compter sur une propriété définie dans le lancement pour vous trouver quelque part (dans mon cas, j'aime garder mes options ouvertes comme Java WebStart - c'est pourquoi j'ai besoin de tout cela ).

Solutions de contournement / améliorations

Spécification du gestionnaire de code manuel

Si vous contrôlez le code, vous pouvez le faire

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

et cela utilisera votre gestionnaire pour ouvrir la connexion.

Mais encore une fois, c'est moins que satisfaisant, car vous n'avez pas besoin d'une URL pour le faire - vous voulez le faire parce qu'une bibliothèque que vous ne pouvez pas (ou ne voulez pas) contrôler veut des URL ...

Inscription du gestionnaire JVM

L'option ultime est d'enregistrer un URLStreamHandlerFactoryqui gérera toutes les URL à travers le 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);
    }
}

Pour enregistrer le gestionnaire, appelez URL.setURLStreamHandlerFactory()avec votre usine configurée. Alors faisnew URL("classpath:org/my/package/resource.extension") comme le premier exemple et c'est parti.

Problème d'enregistrement du gestionnaire JVM

Notez que cette méthode ne peut être appelée qu'une seule fois par JVM, et notez bien que Tomcat utilisera cette méthode pour enregistrer un gestionnaire JNDI (AFAIK). Essayez Jetty (je le serai); au pire, vous pouvez d'abord utiliser la méthode et ensuite elle doit fonctionner autour de vous!

Licence

Je publie ceci dans le domaine public, et je demande que si vous souhaitez modifier que vous démarrez un projet OSS quelque part et commentez ici avec les détails. Une meilleure implémentation serait d'avoir un URLStreamHandlerFactoryqui utilise ThreadLocals pour stocker URLStreamHandlers pour chacun Thread.currentThread().getContextClassLoader(). Je vais même vous donner mes modifications et mes cours de test.

Stephen
la source
1
@Stephen, c'est exactement ce que je recherche. Pouvez-vous s'il vous plaît partager vos mises à jour avec moi? Je peux inclure dans mon com.github.fommil.common-utilspackage que je prévois de mettre à jour et de publier bientôt via Sonatype.
fommil
5
Notez que vous pouvez également utiliser System.setProperty()pour enregistrer le protocole. CommeSystem.setProperty("java.protocol.handler.pkgs", "org.my.protocols");
tsauerwein
Java 9+ a une méthode plus simple: stackoverflow.com/a/56088592/511976
mhvelplund
100
URL url = getClass().getClassLoader().getResource("someresource.xxx");

Ça devrait le faire.

Rasin
la source
11
"Je peux le faire en utilisant Class.getResourceAsStream (" a.xml "), bien sûr, mais cela nécessiterait que j'utilise une API différente, et donc des modifications du code existant. Je veux pouvoir l'utiliser dans tous les endroits où Je peux déjà spécifier une URL pour la ressource, simplement en mettant à jour un fichier de propriétés. "
Thilo
3
-1 Comme l'a souligné Thilo, c'est quelque chose que le PO a considéré et rejeté.
sleske
13
getResource et getResourceAsStream sont des méthodes différentes. Convenu que getResourceAsStream ne correspond pas à l'API, mais getResource renvoie une URL, qui est exactement ce que OP a demandé.
romacafe
@romacafe: Oui, vous avez raison. Il s'agit d'une bonne solution alternative.
sleske
2
OP a demandé une solution de dossier de propriété, mais d'autres viennent également ici en raison du titre de la question. Et ils aiment cette solution dynamique :)
Jarekczek
14

Je pense que cela vaut sa propre réponse - si vous utilisez Spring, vous l'avez déjà avec

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

Comme expliqué dans la documentation du printemps et souligné dans les commentaires de skaffman.

eis
la source
Le printemps à ResourceLoader.getResource()ApplicationContext.getResource()
mon humble avis
11

Vous pouvez également définir la propriété par programme lors du démarrage:

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

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

Ainsi, vous obtenez le moyen le moins intrusif de le faire. :) java.net.URL utilisera toujours la valeur actuelle des propriétés du système.

subes
la source
1
Le code qui ajoute un package supplémentaire pour la recherche à la java.protocol.handler.pkgsvariable système ne peut être utilisé que si le gestionnaire visait à traiter un protocole pas encore "connu" tel que gopher://. Si l'intention est de remplacer le protocole "populaire", comme file://ou http://, il pourrait être trop tard pour le faire, car java.net.URL#handlersmap est déjà ajouté un gestionnaire "standard" pour ce protocole. La seule solution consiste donc à transmettre cette variable à JVM.
dma_k
6

(Similaire à la réponse d' Azder , mais un tact légèrement différent.)

Je ne crois pas qu'il existe un gestionnaire de protocole prédéfini pour le contenu du chemin de classe. (Le soi-disant classpath:protocole).

Cependant, Java vous permet d'ajouter vos propres protocoles. Cela se fait en fournissant des implémentations concrètes java.net.URLStreamHandleret java.net.URLConnection.

Cet article décrit comment un gestionnaire de flux personnalisé peut être implémenté: http://java.sun.com/developer/onlineTraining/protocolhandlers/ .

Dilum Ranatunga
la source
4
Connaissez-vous une liste des protocoles livrés avec la JVM?
Thilo
5

J'ai créé une classe qui aide à réduire les erreurs de configuration des gestionnaires personnalisés et tire parti de la propriété système, il n'y a donc aucun problème à appeler une méthode en premier ou à ne pas être dans le bon conteneur. Il y a aussi une classe d'exception si vous vous trompez:

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
la source
5

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

Utiliser

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

il suffit de créer cette classe dans le sun.net.www.protocol.classpathpackage et de l'exécuter dans l'implémentation JVM Oracle pour fonctionner comme un charme.

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

Si vous utilisez une autre implémentation JVM, définissez la java.protocol.handler.pkgs=sun.net.www.protocolpropriété système.

Pour info: http://docs.oracle.com/javase/7/docs/api/java/net/URL.html#URL(java.lang.String,%20java.lang.String,%20int,%20java.lang .Chaîne)

Daniel De León
la source
3

La solution avec l'enregistrement d'URLStreamHandlers est la plus correcte, bien sûr, mais parfois la solution la plus simple est nécessaire. Donc, j'utilise la méthode suivante pour cela:

/**
 * 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
la source
3

Depuis Java 9+ et plus, vous pouvez définir un nouveau URLStreamHandlerProvider. leURL classe utilise le framework de chargeur de service pour le charger au moment de l'exécution.

Créez un fournisseur:

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

}

Créez un fichier appelé java.net.spi.URLStreamHandlerProviderdans le META-INF/servicesrépertoire avec le contenu:

org.example.ClasspathURLStreamHandlerProvider

Maintenant, la classe URL utilisera le fournisseur lorsqu'elle verra quelque chose comme:

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

Je ne sais pas s'il y en a déjà un, mais vous pouvez le faire vous-même facilement.

Cet exemple de protocoles différents me ressemble à un motif de façade. Vous disposez d'une interface commune lorsqu'il existe différentes implémentations pour chaque cas.

Vous pouvez utiliser le même principe, créer une classe ResourceLoader qui prend la chaîne de votre fichier de propriétés et recherche un de nos protocoles personnalisés

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

supprime le myprotocole: dès le début de la chaîne, puis décide de la manière de charger la ressource et vous donne simplement la ressource.

Azder
la source
Cela ne fonctionne pas si vous souhaitez qu'une bibliothèque tierce utilise des URL et que vous souhaitiez peut-être gérer la résolution des ressources pour un protocole particulier.
mP.
2

Une extension de la réponse de Dilums :

Sans changer le code, vous devrez probablement poursuivre les implémentations personnalisées des interfaces liées aux URL comme le recommande Dilum. Pour simplifier les choses pour vous, je peux recommander de regarder la source des ressources de Spring Framework . Bien que le code ne soit pas sous la forme d'un gestionnaire de flux, il a été conçu pour faire exactement ce que vous cherchez à faire et est sous la licence ASL 2.0, ce qui le rend assez convivial pour être réutilisé dans votre code avec tout le crédit nécessaire.

DavidValeri
la source
Cette page à laquelle vous faites référence indique qu '"il n'y a pas d'implémentation d'URL normalisée qui puisse être utilisée pour accéder à une ressource qui doit être obtenue à partir du chemin de classe ou relative à un ServletContext", ce qui répond à ma question, je suppose.
Thilo
@Sans-abri: accrochez-vous, jeune homme. Avec un peu plus d'expérience, vous publierez bientôt des commentaires en un rien de temps.
Cuga
1

Dans une application Spring Boot, j'ai utilisé ce qui suit pour obtenir l'URL du fichier,

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

Si vous avez tomcat sur le chemin de classe, c'est aussi simple que:

TomcatURLStreamHandlerFactory.register();

Cela enregistrera les gestionnaires pour les protocoles "war" et "classpath".

Olivier Gérardin
la source
0

J'essaie d'éviter la URLclasse et me fie plutôt à moi URI. Ainsi, pour les choses qui ont besoin de l' URLendroit où je voudrais faire Spring Resource comme une recherche sans Spring, je fais ce qui suit:

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

Pour créer un URI, vous pouvez utiliser URI.create(..). Cette méthode est également meilleure car vous contrôlez la fonction ClassLoaderqui effectuera la recherche de ressources.

J'ai remarqué d'autres réponses essayant d'analyser l'URL en tant que chaîne pour détecter le schéma. Je pense qu'il vaut mieux passer l'URI et l'utiliser à la place.

J'ai en fait déposé un problème il y a quelque temps auprès de Spring Source en les suppliant de séparer leur code de ressource coreafin que vous n'ayez pas besoin de tous les autres trucs de Spring.

Adam Gent
la source