web-dev-qa-db-fra.com

URL permettant de charger des ressources à partir du classpath en Java

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

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 joliment le chargement réel de la ressource de l’application qui en a besoin, puisqu’une URL n’est qu’une chaîne, le chargement de la ressource est également très facilement configurable.

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

Je peux le faire en utilisant Class.getResourceAsStream("a.xml") , bien sûr, mais cela me demanderait d’utiliser une API différente et donc de modifier le 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.

182
Thilo

Intro et implémentation de base

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

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 un peu comme moi, vous ne voulez pas vous fier à une propriété définie dans le lancement pour vous emmener quelque part (dans mon cas, j'aime laisser mes options ouvertes comme Java WebStart - ce qui explique pourquoiIbesoin de tout ça).

Solutions de contournement/améliorations

Spécification manuelle du gestionnaire de code

Si vous contrôlez le code, vous pouvez 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 que certaines bibliothèques vous ne pouvez pas (ou ne voulez pas) contrôler les URL souhaitées ...

Enregistrement du gestionnaire JVM

L'option ultime consiste à enregistrer une URLStreamHandlerFactory qui gérera toutes les URL du 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 fabrique configurée. Ensuite, faites new 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 fois par machine virtuelle et notez bien que Tomcat l'utilisera pour enregistrer un gestionnaire JNDI (autant que je sache). Essayez Jetty (je le serai); dans le pire des cas, vous pouvez utiliser la méthode d'abord et ensuite, elle doit fonctionner autour de vous!

Licence

Je libère ceci dans le domaine public, et demande si vous souhaitez modifier que vous démarriez un projet OSS quelque part et commentez ici avec les détails. Une meilleure implémentation serait d'avoir une URLStreamHandlerFactory qui utilise ThreadLocals pour stocker URLStreamHandlers pour chaque Thread.currentThread().getContextClassLoader(). Je vais même vous donner mes modifications et les classes de test.

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

Ça devrait le faire. 

88
Rasin

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

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

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

11
eis

Vous pouvez également définir la propriété par programmation 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);

En utilisant 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 système.

9
subes

J'ai créé une classe qui aide à réduire les erreurs lors de la configuration des gestionnaires personnalisés et tire parti de la propriété système pour éviter tout problème d'appeler une méthode en premier ou de ne pas figurer 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 );
    }

}
4
Jason Wraxall

(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 classpath. (Le soi-disant protocole classpath:).

Cependant, Java vous permet d'ajouter vos propres protocoles. Ceci est fait en fournissant des implémentations concrètes Java.net.URLStreamHandler et Java.net.URLConnection .

Cet article explique comment un gestionnaire de flux personnalisé peut être implémenté:  http://Java.Sun.com/developer/onlineTraining/protocolhandlers/.

4
Dilum Ranatunga

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

créez simplement cette classe dans le package Sun.net.www.protocol.classpath et exécutez-la dans l'implémentation JVM d'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 propriété système 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.String)

3
Daniel De León

Une extension de La réponse de Dilums :

Sans changer de code, il est probablement nécessaire de poursuivre l'implémentation personnalisée d'interfaces liées aux URL, comme le recommande Dilum. Pour vous simplifier la vie, je vous recommande de consulter la source de Ressources de Spring Framework . Bien que le code ne se présente pas sous la forme d'un gestionnaire de flux, il a été conçu pour faire exactement ce que vous cherchez et est sous licence ASL 2.0, le rendant ainsi assez convivial pour le réutiliser correctement dans votre code.

2
DavidValeri

La solution avec l'enregistrement de URLStreamHandlers est bien sûr la solution la plus correcte, 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;
}
2
domax

À partir de Java 9+, vous pouvez définir une nouvelle URLStreamHandlerProvider. La classe URL utilise la structure du chargeur de service pour le charger au moment de l'exécution.

Créer 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.URLStreamHandlerProvider dans le répertoire META-INF/services avec le contenu:

org.example.ClasspathURLStreamHandlerProvider

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

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

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

Cet exemple de protocole différent me ressemble à un motif de façade. Vous avez 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 extrait la chaîne de votre fichier de propriétés et vérifie si notre protocole est personnalisé.

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 myprotocol: à partir du début de la chaîne, puis décide du mode de chargement de la ressource et vous donne simplement la ressource.

1
Azder

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

TomcatURLStreamHandlerFactory.register();

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

1
Olivier Gérardin

J'essaie d'éviter la classe URL et je m'appuie plutôt sur URI. Ainsi, pour les choses qui ont besoin de URL où je voudrais faire Spring Resource comme lookup 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 préférable, car vous contrôlez la ClassLoader qui effectuera la recherche des ressources.

J'ai remarqué d'autres réponses essayant d'analyser l'URL sous forme de chaîne pour détecter le schéma. Je pense que c'est mieux de passer l'URI et de l'utiliser à la place pour analyser.

J'ai récemment déposé un problème avec Spring Source, les priant de séparer leur code de ressource de core afin que vous n'ayez pas besoin de tous les autres éléments de Spring.

0
Adam Gent

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

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