URL para cargar recursos desde el classpath en Java

197

En Java, puede cargar todo tipo de recursos utilizando la misma API pero con diferentes protocolos de URL:

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

Esto desacopla muy bien la carga real del recurso de la aplicación que lo necesita, y dado que una URL es solo una cadena, la carga de recursos también es muy fácilmente configurable.

¿Existe un protocolo para cargar recursos utilizando el cargador de clases actual? Esto es similar al protocolo Jar, excepto que no necesito saber de qué archivo jar o carpeta de clase proviene el recurso.

Puedo hacerlo usando Class.getResourceAsStream("a.xml"), por supuesto, pero eso requeriría que use una API diferente y, por lo tanto, cambios en el código existente. Quiero poder usar esto en todos los lugares donde ya pueda especificar una URL para el recurso, simplemente actualizando un archivo de propiedades.

Thilo
fuente

Respuestas:

348

Introducción e implementación básica

Primero, necesitará al menos un URLStreamHandler. Esto realmente abrirá la conexión a una URL dada. Tenga en cuenta que esto simplemente se llama Handler; esto le permite especificar java -Djava.protocol.handler.pkgs=org.my.protocolsy se recogerá automáticamente, utilizando el nombre del paquete "simple" como protocolo compatible (en este caso, "classpath").

Uso

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

Código

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

Problemas de lanzamiento

Si eres como yo, no quieres depender de una propiedad establecida en el lanzamiento para llevarte a algún lugar (en mi caso, me gusta mantener mis opciones abiertas como Java WebStart, por eso necesito todo esto )

Soluciones / Mejoras

Código manual Especificación del controlador

Si controlas el código, puedes hacer

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

y esto usará su controlador para abrir la conexión.

Pero, de nuevo, esto es menos que satisfactorio, ya que no necesita una URL para hacer esto; desea hacerlo porque alguna biblioteca que no puede (o no quiere) controlar quiere URL ...

Registro de controlador JVM

La última opción es registrar un URLStreamHandlerFactoryque maneje todas las URL en la 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);
    }
}

Para registrar el controlador, llame URL.setURLStreamHandlerFactory()a su fábrica configurada. Entonces haz new URL("classpath:org/my/package/resource.extension")como el primer ejemplo y listo.

Problema de registro del controlador JVM

Tenga en cuenta que este método solo se puede llamar una vez por JVM, y tenga en cuenta que Tomcat utilizará este método para registrar un controlador JNDI (AFAIK). Prueba Jetty (lo estaré); en el peor de los casos, puede usar el método primero y luego tiene que funcionar a su alrededor.

Licencia

Lanzo esto al dominio público y le pido que, si desea modificarlo, inicie un proyecto OSS en algún lugar y comente aquí con los detalles. Una mejor implementación sería tener una URLStreamHandlerFactoryque use ThreadLocals para almacenar URLStreamHandlers para cada uno Thread.currentThread().getContextClassLoader(). Incluso te daré mis modificaciones y clases de prueba.

Stephen
fuente
1
@Stephen esto es exactamente lo que estoy buscando. ¿Puedes compartir tus actualizaciones conmigo? Puedo incluir como parte de mi com.github.fommil.common-utilspaquete que planeo actualizar y lanzar pronto a través de Sonatype.
fommil
55
Tenga en cuenta que también puede usar System.setProperty()para registrar el protocolo. Me gustaSystem.setProperty("java.protocol.handler.pkgs", "org.my.protocols");
tsauerwein 05 de
Java 9+ tiene un método más fácil: stackoverflow.com/a/56088592/511976
mhvelplund
100
URL url = getClass().getClassLoader().getResource("someresource.xxx");

Deberias hacer eso.

Rasin
fuente
11
"Puedo hacer eso usando Class.getResourceAsStream (" a.xml "), por supuesto, pero eso requeriría que use una API diferente y, por lo tanto, cambios en el código existente. Quiero poder usar esto en todos los lugares donde Ya puedo especificar una URL para el recurso, simplemente actualizando un archivo de propiedades ".
Thilo
3
-1 Como señaló Thilo, esto es algo que el OP consideró y rechazó.
sleske
13
getResource y getResourceAsStream son métodos diferentes. Convino en que getResourceAsStream no se ajusta a la API, pero getResource devuelve una URL, que es exactamente lo que solicitó OP.
romacafe
@romacafé: Sí, tienes razón. Esta es una buena solución alternativa.
sleske
2
OP solicitó una solución de archivo de propiedad, pero otros también vienen aquí por el título de la pregunta. Y les gusta esta solución dinámica :)
Jarekczek
14

Creo que esto vale su propia respuesta: si está utilizando Spring, ya lo tiene con

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

Como se explica en la documentación de primavera y se señala en los comentarios de skaffman.

eis
fuente
En mi humilde opinión, la primavera ResourceLoader.getResource()es más apropiada para la tarea (los ApplicationContext.getResource()delegados a ella bajo el capó)
Lu55
11

También puede establecer la propiedad mediante programación durante el inicio:

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

Usando esta clase:

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

Por lo tanto, obtienes la forma menos intrusiva de hacer esto. :) java.net.URL siempre usará el valor actual de las propiedades del sistema.

subes
fuente
1
El código que agrega un paquete adicional para la búsqueda de java.protocol.handler.pkgsvariables del sistema solo se puede usar si el controlador apunta a procesar un protocolo aún no "conocido" como gopher://. Si la intención es anular el protocolo "popular", como file://o http://, podría ser demasiado tarde para hacerlo, ya que el java.net.URL#handlersmapa ya ha agregado un controlador "estándar" para ese protocolo. Entonces, la única salida es pasar esta variable a JVM.
dma_k
6

(Similar a la respuesta de Azder , pero con un tacto ligeramente diferente).

No creo que haya un controlador de protocolo predefinido para el contenido del classpath. (El llamado classpath:protocolo).

Sin embargo, Java le permite agregar sus propios protocolos. Esto se hace mediante la implementación de implementaciones concretas java.net.URLStreamHandlery java.net.URLConnection.

Este artículo describe cómo se puede implementar un controlador de flujo personalizado: http://java.sun.com/developer/onlineTraining/protocolhandlers/ .

Dilum Ranatunga
fuente
44
¿Conoces una lista de los protocolos que se envían con la JVM?
Thilo
5

He creado una clase que ayuda a reducir los errores en la configuración de controladores personalizados y aprovecha la propiedad del sistema para que no haya problemas al llamar a un método primero o no estar en el contenedor correcto. También hay una clase de excepción si te equivocas:

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

Inspírate en @Stephen https://stackoverflow.com/a/1769454/980442 y http://docstore.mik.ua/orelly/java/exp/ch09_06.htm

Usar

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

solo cree esta clase en un sun.net.www.protocol.classpathpaquete y ejecútelo en la implementación de Oracle JVM para que funcione como un encanto.

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

En caso de que esté utilizando otra implementación de JVM, configure la java.protocol.handler.pkgs=sun.net.www.protocolpropiedad del sistema.

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

Daniel De León
fuente
3

La solución con el registro de URLStreamHandlers es la más correcta, por supuesto, pero a veces se necesita la solución más simple. Entonces, utilizo el siguiente método para eso:

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

Desde Java 9+ y superior, puede definir uno nuevo URLStreamHandlerProvider. losURL clase utiliza el marco del cargador de servicios para cargarlo en tiempo de ejecución.

Crea un proveedor:

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

}

Cree un archivo llamado java.net.spi.URLStreamHandlerProvideren el META-INF/servicesdirectorio con el contenido:

org.example.ClasspathURLStreamHandlerProvider

Ahora la clase URL usará el proveedor cuando vea algo como:

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

No sé si ya hay uno, pero puedes hacerlo tú mismo fácilmente.

Ese ejemplo de protocolos diferentes me parece un patrón de fachada. Tiene una interfaz común cuando hay implementaciones diferentes para cada caso.

Podría usar el mismo principio, crear una clase ResourceLoader que tome la cadena de su archivo de propiedades y verifique si hay un protocolo personalizado nuestro

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 el myprotocol: desde el inicio de la cadena y luego toma una decisión sobre la forma de cargar el recurso, y simplemente le da el recurso.

Azder
fuente
Esto no funciona si desea que una biblioteca de terceros use URL y tal vez desee manejar la resolución de recursos para un protocolo en particular.
mP.
2

Una extensión a la respuesta de Dilums :

Sin cambiar el código, es probable que necesite implementar implementaciones personalizadas de interfaces relacionadas con URL como recomienda Dilum. Para simplificar las cosas para usted, puedo recomendar mirar la fuente de los recursos de Spring Framework . Si bien el código no tiene la forma de un controlador de flujo, ha sido diseñado para hacer exactamente lo que está buscando hacer y está bajo la licencia ASL 2.0, lo que lo hace lo suficientemente amigable para su reutilización en su código con el debido crédito.

DavidValeri
fuente
Esa página a la que hace referencia indica que "no existe una implementación de URL estandarizada que pueda usarse para acceder a un recurso que necesita obtenerse del classpath, o relativo a un ServletContext", lo que responde a mi pregunta, supongo.
Thilo
@Homeless: aguanta, joven. Con un poco más de experiencia, pronto publicará comentarios en poco tiempo.
Cuga
1

En una aplicación Spring Boot, utilicé lo siguiente para obtener la URL del archivo,

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

Si tienes tomcat en el classpath, es tan simple como:

TomcatURLStreamHandlerFactory.register();

Esto registrará controladores para los protocolos "war" y "classpath".

Olivier Gérardin
fuente
0

Intento evitar la URLclase y en su lugar confío URI. Por lo tanto, para las cosas que necesitan URLdonde me gustaría hacer Spring Resource, como la búsqueda sin Spring, hago lo siguiente:

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

Para crear un URI que puede usar URI.create(..). De esta manera también es mejor porque controlas lo ClassLoaderque hará la búsqueda de recursos.

Noté algunas otras respuestas tratando de analizar la URL como una cadena para detectar el esquema. Creo que es mejor pasar URI y usarlo para analizar en su lugar.

De hecho, he presentado un problema hace un tiempo con Spring Source rogándoles que separen su código de Recursos corepara que no necesites todas las demás cosas de Spring.

Adam Gent
fuente