URL para cargar recursos desde el classpath en Java

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 necesita el recurso, y dado que una URL es solo una cadena, la carga de recursos también es muy fácil de configurar.

¿Hay un protocolo para cargar recursos usando 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, cambie el código existente. Quiero poder utilizar esto en todos los lugares donde puedo especificar una URL para el recurso, simplemente actualizando un archivo de propiedad.

Introducción e implementación básica

Primero, necesitarás 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.protocols y se seleccionará 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(); } } 

Lanzar problemas

Si eres como yo, no quieres confiar en que una propiedad se establezca en el lanzamiento para llevarte a algún lado (en mi caso, me gusta mantener mis opciones abiertas como Java WebStart, por lo que necesito todo esto). )

Soluciones / Mejoras

Especificación manual del controlador de código

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á tu controlador para abrir la conexión.

Pero, una vez más, esto es menos que satisfactorio, ya que no necesita una URL para hacer esto: desea hacer esto porque una lib no puede (o no quiere) controlar las URL deseadas …

Registro de manejador JVM

La última opción es registrar un URLStreamHandlerFactory que manejará todas las URL a través de 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 protocolHandlers; public ConfigurableStreamHandlerFactory(String protocol, URLStreamHandler urlHandler) { protocolHandlers = new HashMap(); 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 a URL.setURLStreamHandlerFactory() con su fábrica configurada. Luego haga una new URL("classpath:org/my/package/resource.extension") como el primer ejemplo y listo.

Problema de registro de manejador 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 seré); en el peor, puede usar el método primero y luego tiene que trabajar a su alrededor.

Licencia

Lo libero al dominio público y le pido que, si desea modificarlo, comience un proyecto de OSS en algún lugar y comente aquí con los detalles. Una mejor implementación sería tener un URLStreamHandlerFactory que use ThreadLocal s para almacenar URLStreamHandler s para cada Thread.currentThread().getContextClassLoader() . Incluso te daré mis modificaciones y clases de prueba.

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

Deberias hacer eso.

Creo que vale la pena su propia respuesta: si usas Spring, ya tienes esto 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 spring y se señala en los comentarios de skaffman.

También puede configurar la propiedad mediante progtwigció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.

Creé una clase que ayuda a reducir los errores en la configuración de manejadores personalizados y aprovecha la propiedad del sistema para que no haya problemas con llamar primero a un método o no estar en el contenedor correcto. También hay una clase de excepción si obtienes cosas incorrectas:

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

(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 proporcionando implementaciones concretas java.net.URLStreamHandler y java.net.URLConnection .

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

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, uso el siguiente método para eso:

 /** * Opens a local file or remote resource represented by given path. * Supports protocols: * 
    *
  • "file": file:///path/to/file/in/filesystem
  • *
  • "http" or "https": http://host/path/to/resource - gzipped resources are supported also
  • *
  • "classpath": classpath:path/to/resource
  • *
* * @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; }

Inspirar por @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() 

simplemente cree esta clase en el paquete sun.net.www.protocol.classpath y ejecútelo en la implementación Oracle JVM para que funcione como un amuleto.

 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 propiedad del sistema 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 .Cuerda)

Una extensión de la respuesta de Dilums :

Sin cambiar el código, es probable que necesite buscar implementaciones personalizadas de interfaces relacionadas con URL como recomienda Dilum. Para simplificar las cosas, recomiendo consultar la fuente de los recursos de Spring Framework . Si bien el código no tiene forma de controlador de flujo, se diseñó para hacer exactamente lo que usted busca hacer y está bajo la licencia ASL 2.0, lo que lo hace lo suficientemente amigable como para volver a usarlo en su código con el debido crédito.

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. Usted tiene una interfaz común cuando hay diferentes implementaciones para cada caso.

Podría usar el mismo principio, crear una clase ResourceLoader que tome la cadena de su archivo de propiedades y verifique 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 

quita el myprotocol: desde el comienzo de la cadena y luego toma una decisión sobre la forma de cargar el recurso, y solo te da el recurso.

Intento evitar la clase de URL y, en cambio, confiar en el URI . Por lo tanto, para cosas que necesitan URL donde me gustaría hacer Spring Resource como 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 puede usar URI.create(..) . De esta manera también es mejor porque usted controla el ClassLoader que 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 el URI y utilizarlo para analizar.

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

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

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