URL для загрузки ресурсов из пути к классам в Java
В Java вы можете загружать все виды ресурсов, используя один и тот же API, но с разными протоколами URL:
file:///tmp.txt
http://127.0.0.1:8080/a.properties
jar:http://www.foo.com/bar/baz.jar!/COM/foo/Quux.class
Это прекрасно отделяет фактическую загрузку ресурса от приложения, которому нужен ресурс, и поскольку URL-адрес является просто строкой, загрузка ресурса также очень легко настраивается.
Есть ли протокол для загрузки ресурсов с использованием текущего загрузчика классов? Это похоже на протокол Jar, за исключением того, что мне не нужно знать, из какого файла JAR или папки классов поступает ресурс.
Я могу сделать это с помощью Class.getResourceAsStream("a.xml")
Конечно, но это потребовало бы от меня использования другого API и, следовательно, изменений в существующем коде. Я хочу иметь возможность использовать это во всех местах, где я уже могу указать URL для ресурса, просто обновив файл свойств.
13 ответов
Введение и базовая реализация
Во-первых, вам понадобится хотя бы URLStreamHandler. Это фактически откроет соединение с заданным URL. Обратите внимание, что это просто называется Handler
; это позволяет вам указать java -Djava.protocol.handler.pkgs=org.my.protocols
и он будет автоматически выбран, используя "простое" имя пакета в качестве поддерживаемого протокола (в данном случае "classpath").
использование
new URL("classpath:org/my/package/resource.extension").openConnection();
Код
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();
}
}
Проблемы с запуском
Если вы чем-то похожи на меня, вы не хотите полагаться на свойство, установленное при запуске, чтобы получить вас куда-то (в моем случае я хотел бы оставить свои параметры открытыми, как Java WebStart - вот почему мне нужно все это).Обходные пути / Улучшения
Ручной код Спецификация обработчика
Если вы контролируете код, вы можете сделать
new URL(null, "classpath:some/package/resource.extension", new org.my.protocols.classpath.Handler(ClassLoader.getSystemClassLoader()))
и это будет использовать ваш обработчик, чтобы открыть соединение.
Но опять же, это менее чем удовлетворительно, так как вам не нужен URL для этого - вы хотите сделать это, потому что некоторая библиотека, которую вы не можете (или не хотите) контролировать, требует URL...
Регистрация обработчика JVM
Окончательный вариант - зарегистрировать URLStreamHandlerFactory
это будет обрабатывать все URL через 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);
}
}
Чтобы зарегистрировать обработчик, позвоните URL.setURLStreamHandlerFactory()
с вашим настроенным заводом. Тогда делай new URL("classpath:org/my/package/resource.extension")
как первый пример, и понеслось.
Проблема регистрации обработчика JVM
Обратите внимание, что этот метод может вызываться только один раз для JVM, и обратите внимание, что Tomcat будет использовать этот метод для регистрации обработчика JNDI (AFAIK). Попробуйте Jetty (я буду); в худшем случае вы можете сначала использовать метод, а затем он должен работать вокруг вас!
Лицензия
Я передаю это в общественное достояние и спрашиваю, что если вы хотите изменить это, вы запускаете проект OSS где-то и комментируете здесь с деталями. Лучшая реализация будет иметь URLStreamHandlerFactory
который использует ThreadLocal
с хранить URLStreamHandler
для каждого Thread.currentThread().getContextClassLoader()
, Я даже дам вам свои модификации и тестовые классы.
URL url = getClass().getClassLoader().getResource("someresource.xxx");
Это должно сделать это.
Начиная с Java 9+ и выше, вы можете определить новый URLStreamHandlerProvider
. ВURL
Класс использует платформу загрузчика сервисов для загрузки во время выполнения.
Создайте провайдера:
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;
}
}
Создайте файл с именем java.net.spi.URLStreamHandlerProvider
в META-INF/services
каталог с содержимым:
org.example.ClasspathURLStreamHandlerProvider
Теперь класс URL будет использовать поставщика, когда увидит что-то вроде:
URL url = new URL("classpath:myfile.txt");
Я думаю, что это стоит своего ответа - если вы используете Spring, у вас уже есть это с
Resource firstResource =
context.getResource("http://www.google.fi/");
Resource anotherResource =
context.getResource("classpath:some/resource/path/myTemplate.txt");
Как объяснено в весенней документации и указано в комментариях Скаффмана.
Вы также можете установить свойство программно во время запуска:
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);
Используя этот класс:
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();
}
}
Таким образом, вы получаете наименее навязчивый способ сделать это.:) java.net.URL всегда будет использовать текущее значение из системных свойств.
(Аналогично ответу Аздера, но немного другой такт.)
Я не верю, что есть предопределенный обработчик протокола для содержимого из classpath. (Так называемый classpath:
Протокол).
Однако Java позволяет вам добавлять свои собственные протоколы. Это делается путем предоставления конкретных реализаций java.net.URLStreamHandler
а также java.net.URLConnection
,
В этой статье описывается, как может быть реализован пользовательский обработчик потока: http://java.sun.com/developer/onlineTraining/protocolhandlers/.
Я создал класс, который помогает уменьшить количество ошибок при настройке пользовательских обработчиков и использует преимущества системного свойства, поэтому нет проблем с вызовом метода первым или отсутствием нужного контейнера. Существует также класс исключений, если вы ошибаетесь:
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 );
}
}
Вдохновитесь @Stephen /questions/43172114/url-dlya-zagruzki-resursov-iz-puti-k-klassam-v-java/43172151#43172151 и http://docstore.mik.ua/orelly/java/exp/ch09_06.htm
Использовать
new URL("classpath:org/my/package/resource.extension").openConnection()
просто создайте этот класс в sun.net.www.protocol.classpath
пакет и запустить его в реализации Oracle JVM, чтобы работать как шарм.
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();
}
}
Если вы используете другую реализацию JVM, установите java.protocol.handler.pkgs=sun.net.www.protocol
системное свойство.
К вашему сведению: http://docs.oracle.com/javase/7/docs/api/java/net/URL.html
Конечно, решение с регистрацией URLStreamHandlers является наиболее правильным, но иногда требуется самое простое решение. Итак, я использую следующий метод для этого:
/**
* 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;
}
Я не знаю, есть ли уже, но вы можете сделать это сами легко.
Этот пример другого протокола выглядит для меня как шаблон фасада. У вас есть общий интерфейс, когда есть разные реализации для каждого случая.
Вы можете использовать тот же принцип, создать класс ResourceLoader, который берет строку из вашего файла свойств и проверяет наш собственный протокол.
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
удаляет myprotocol: с начала строки, а затем принимает решение о том, каким способом загрузить ресурс, и просто предоставляет вам ресурс.
Расширение ответа Дилумса:
Без изменения кода вам, скорее всего, нужно следовать пользовательским реализациям интерфейсов, связанных с URL, как рекомендует Dilum. Чтобы упростить для вас вещи, я могу порекомендовать взглянуть на источник ресурсов Spring Framework. Хотя код не в форме потокового обработчика, он был разработан для того, чтобы делать именно то, что вы хотите, и находится под лицензией ASL 2.0, что делает его достаточно дружественным для повторного использования в вашем коде с должным доверием.
В приложении Spring Boot я использовал следующее, чтобы получить URL файла,
Thread.currentThread().getContextClassLoader().getResource("PromotionalOfferIdServiceV2.wsdl")
Если у вас есть tomcat на пути к классам, это так просто:
TomcatURLStreamHandlerFactory.register();
Это зарегистрирует обработчики для протоколов "war" и "classpath".
Я стараюсь избегать URL
класс и вместо этого полагаться на URI
, Таким образом, для вещей, которые нуждаются URL
где я хотел бы сделать Spring Resource как поиск без Spring я делаю следующее:
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();
}
}
Для создания URI вы можете использовать URI.create(..)
, Этот способ также лучше, потому что вы контролируете ClassLoader
это сделает поиск ресурса.
Я заметил несколько других ответов, пытаясь проанализировать URL-адрес как строку для определения схемы. Я думаю, что лучше обойти URI и использовать его для анализа.
Я на самом деле подал проблему недавно с Spring Source, умоляя их отделить свой код ресурса от core
так что вам не нужны все другие вещи Spring.