URL para carregar recursos do caminho de classe em Java

197

Em Java, você pode carregar todos os tipos de recursos usando a mesma API, mas com 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

Isso dissocia muito bem o carregamento real do recurso do aplicativo que precisa do recurso e, como uma URL é apenas uma String, o carregamento do recurso também é facilmente configurável.

Existe um protocolo para carregar recursos usando o carregador de classe atual? Isso é semelhante ao protocolo Jar, exceto que eu não preciso saber de qual arquivo jar ou pasta de classe o recurso vem.

Eu posso fazer isso usando Class.getResourceAsStream("a.xml"), é claro, mas isso exigiria que eu usasse uma API diferente e, portanto, alterações no código existente. Quero poder usar isso em todos os lugares onde já posso especificar uma URL para o recurso, apenas atualizando um arquivo de propriedades.

Thilo
fonte

Respostas:

348

Introdução e Implementação básica

Primeiro, você precisará de pelo menos um URLStreamHandler. Isso realmente abrirá a conexão com um determinado URL. Observe que isso é chamado simplesmente Handler; isso permite que você especifique java -Djava.protocol.handler.pkgs=org.my.protocolse ele será automaticamente escolhido, usando o nome do pacote "simples" como o protocolo suportado (neste caso, "caminho da classe").

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 lançamento

Se você é como eu, não quer depender de uma propriedade configurada no lançamento para levá-lo a algum lugar (no meu caso, gosto de manter minhas opções em aberto como o Java WebStart - é por isso que preciso de tudo isso )

Soluções alternativas / aprimoramentos

Código manual Especificação do manipulador

Se você controlar o código, poderá fazer

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

e isso usará seu manipulador para abrir a conexão.

Mas, novamente, isso é menos do que satisfatório, pois você não precisa de um URL para fazer isso - você quer fazer isso porque alguma lib que você não pode (ou não quer) controlar quer URLs ...

Registro de manipulador da JVM

A opção final é registrar um URLStreamHandlerFactoryque manipulará todos os URLs na 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 o manipulador, ligue URL.setURLStreamHandlerFactory()para a fábrica configurada. Então faça new URL("classpath:org/my/package/resource.extension")o primeiro exemplo e você vai embora.

Problema de registro de manipulador da JVM

Observe que esse método pode ser chamado apenas uma vez por JVM e observe bem que o Tomcat usará esse método para registrar um manipulador JNDI (AFAIK). Tente Jetty (eu serei); na pior das hipóteses, você pode usar o método primeiro e, em seguida, ele deve contornar você!

Licença

Eu libero isso para o domínio público e solicito que, se você deseja modificar, inicie um projeto OSS em algum lugar e comente aqui com os detalhes. Uma implementação melhor seria ter um URLStreamHandlerFactoryque use ThreadLocals para armazenar URLStreamHandlers para cada um Thread.currentThread().getContextClassLoader(). Eu até darei minhas modificações e aulas de teste.

Stephen
fonte
1
@ Stephen, é exatamente isso que estou procurando. Você pode compartilhar suas atualizações comigo? Posso incluir como parte do meu com.github.fommil.common-utilspacote que planejo atualizar e lançar em breve via Sonatype.
fommil 17/02
5
Observe que você também pode usar System.setProperty()para registrar o protocolo. ComoSystem.setProperty("java.protocol.handler.pkgs", "org.my.protocols");
tsauerwein
Java 9+ tem um método mais fácil: stackoverflow.com/a/56088592/511976
mhvelplund
100
URL url = getClass().getClassLoader().getResource("someresource.xxx");

Isso deve resolver.

Rasin
fonte
11
"É possível fazer isso usando Class.getResourceAsStream (" a.xml "), é claro, mas isso exigiria o uso de uma API diferente e, portanto, alterações no código existente. Quero poder usar isso em todos os lugares onde Já posso especificar um URL para o recurso, apenas atualizando um arquivo de propriedades. "
Thilo
3
-1 Como apontado por Thilo, isso é algo que o OP considerou e rejeitou.
sleske
13
getResource e getResourceAsStream são métodos diferentes. Concordou que getResourceAsStream não se encaixa na API, mas getResource retorna uma URL, exatamente o que o OP solicitou.
Romacafe
@romacafe: Sim, você está certo. Esta é uma boa solução alternativa.
sleske
2
O OP pediu a solução do arquivo de propriedades, mas outros também vêm aqui por causa do título da pergunta. E eles gostam dessa solução dinâmica :)
Jarekczek
14

Eu acho que isso vale a sua própria resposta - se você estiver usando o Spring, você já tem isso com

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

Como explicado na documentação da primavera e apontado nos comentários de skaffman.

eis
fonte
Primavera IMHO ResourceLoader.getResource()é mais apropriado para a tarefa ( ApplicationContext.getResource()delegados a ele sob o capô)
Lu55
11

Você também pode definir a propriedade programaticamente durante a inicialização:

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

Assim, você obtém a maneira menos invasiva de fazer isso. :) java.net.URL sempre usará o valor atual das propriedades do sistema.

subes
fonte
1
O código que adiciona pacote extra para pesquisa na java.protocol.handler.pkgsvariável do sistema só pode ser usado se o manipulador tiver como objetivo processar protocolos ainda não "conhecidos", como gopher://. Se a intenção é substituir o protocolo "popular", como file://ou http://, pode ser tarde demais para fazê-lo, pois o java.net.URL#handlersmap já possui um manipulador "padrão" para esse protocolo. Portanto, a única saída é passar essa variável para a JVM.
dma_k
6

(Semelhante à resposta de Azder , mas com um tato um pouco diferente.)

Não acredito que exista um manipulador de protocolo predefinido para o conteúdo do caminho de classe. (O assim chamadoclasspath: protocolo).

No entanto, o Java permite adicionar seus próprios protocolos. Isso é feito através do fornecimento de implementações concretas java.net.URLStreamHandlere java.net.URLConnection.

Este artigo descreve como um manipulador de fluxo personalizado pode ser implementado: http://java.sun.com/developer/onlineTraining/protocolhandlers/ .

Dilum Ranatunga
fonte
4
Você conhece uma lista de quais protocolos são fornecidos com a JVM?
Thilo
5

Criei uma classe que ajuda a reduzir erros na configuração de manipuladores personalizados e tira proveito da propriedade do sistema, para que não haja problemas em chamar um método primeiro ou não estar no contêiner certo. Há também uma classe de exceção se você errar:

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

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

Usar

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

basta criar essa classe no sun.net.www.protocol.classpathpacote e executá-la na implementação do Oracle JVM para funcionar como um 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();
    }
}

Caso você esteja usando outra implementação da JVM, configure a java.protocol.handler.pkgs=sun.net.www.protocolpropriedade do 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)

Daniel De León
fonte
3

A solução com o registro de URLStreamHandlers é a mais correta, é claro, mas às vezes a solução mais simples é necessária. Então, eu uso o seguinte método para isso:

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

Do Java 9+ ou superior, você pode definir um novo URLStreamHandlerProvider. oURL classe usa a estrutura do carregador de serviço para carregá-la em tempo de execução.

Crie um provedor:

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

}

Crie um arquivo chamado java.net.spi.URLStreamHandlerProviderno META-INF/servicesdiretório com o conteúdo:

org.example.ClasspathURLStreamHandlerProvider

Agora, a classe de URL usará o provedor quando vir algo como:

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

Não sei se já existe um, mas você pode fazê-lo facilmente.

Esse exemplo de protocolos diferentes me parece um padrão de fachada. Você tem uma interface comum quando há implementações diferentes para cada caso.

Você pode usar o mesmo princípio, criar uma classe ResourceLoader que retire a string do seu arquivo de propriedades e verifique um protocolo personalizado nosso

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

retira o myprotocol: desde o início da sequência e, em seguida, toma uma decisão de como carregar o recurso e fornece apenas o recurso.

Azder
fonte
Isso não funciona se você deseja que uma lib de terceiros use URLs e talvez queira lidar com a resolução de recursos para um protocolo específico.
mP.
2

Uma extensão da resposta de Dilums :

Sem alterar o código, você provavelmente precisará implementar implementações personalizadas de interfaces relacionadas à URL, como recomenda a Dilum. Para simplificar as coisas para você, recomendo procurar a fonte dos Recursos do Spring Framework . Embora o código não esteja na forma de um manipulador de fluxo, ele foi projetado para fazer exatamente o que você está procurando e está sob a licença ASL 2.0, tornando-o amigável o suficiente para reutilizar seu código com o devido crédito.

DavidValeri
fonte
Essa página que você menciona afirma que "não existe uma implementação padronizada de URL que possa ser usada para acessar um recurso que precisa ser obtido no caminho de classe ou relativo a um ServletContext", que responde minha pergunta, eu acho.
Thilo
@ Homeless: espere aí, jovem. Com um pouco mais de experiência, em breve você postará comentários.
Cuga 15/05/09
1

Em um aplicativo Spring Boot, usei o seguinte para obter o URL do arquivo,

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

Se você tiver o tomcat no caminho de classe, é tão simples quanto:

TomcatURLStreamHandlerFactory.register();

Isso registrará manipuladores para os protocolos "war" e "classpath".

Olivier Gérardin
fonte
0

Eu tento evitar a URLaula e, em vez disso, confio URI. Assim, para coisas que precisam de URLonde eu gostaria de fazer o Spring Resource, como pesquisa sem o Spring, faço o seguinte:

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 criar um URI, você pode usar URI.create(..). Dessa forma, também é melhor porque você controla o ClassLoaderque fará a pesquisa de recursos.

Notei algumas outras respostas tentando analisar o URL como uma String para detectar o esquema. Eu acho que é melhor passar pelo URI e usá-lo para analisar.

Na verdade, eu arquivei um problema há um tempo atrás, com o Spring Source implorando para que eles separassem seu código de recurso, corepara que você não precisasse de todos os outros itens do Spring.

Adam Gent
fonte