URL untuk memuat sumber daya dari classpath di Jawa

197

Di Jawa, Anda dapat memuat semua jenis sumber daya menggunakan API yang sama tetapi dengan protokol URL yang berbeda:

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

Ini dengan baik memisahkan pemuatan aktual sumber daya dari aplikasi yang membutuhkan sumber daya, dan karena URL hanyalah sebuah String, pemuatan sumber daya juga sangat mudah dikonfigurasi.

Apakah ada protokol untuk memuat sumber daya menggunakan classloader saat ini? Ini mirip dengan protokol Jar, kecuali bahwa saya tidak perlu tahu file jar atau folder kelas mana sumbernya berasal.

Saya bisa melakukannya dengan menggunakan Class.getResourceAsStream("a.xml"), tentu saja, tetapi itu akan mengharuskan saya untuk menggunakan API yang berbeda, dan karenanya mengubah kode yang ada. Saya ingin dapat menggunakan ini di semua tempat di mana saya dapat menentukan URL untuk sumber daya, dengan hanya memperbarui file properti.

Thilo
sumber

Jawaban:

348

Implementasi Intro dan dasar

Pertama, Anda membutuhkan setidaknya URLStreamHandler. Ini sebenarnya akan membuka koneksi ke URL yang diberikan. Perhatikan bahwa ini hanya disebut Handler; ini memungkinkan Anda untuk menentukan java -Djava.protocol.handler.pkgs=org.my.protocolsdan secara otomatis akan diambil, menggunakan nama paket "sederhana" sebagai protokol yang didukung (dalam hal ini "classpath").

Pemakaian

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

Kode

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

Luncurkan masalah

Jika Anda seperti saya, Anda tidak ingin bergantung pada properti yang diatur dalam peluncuran untuk membawa Anda ke suatu tempat (dalam kasus saya, saya ingin menjaga opsi saya tetap terbuka seperti Java WebStart - itulah sebabnya saya membutuhkan semua ini ).

Penanganan / Peningkatan

Spesifikasi Penangan kode manual

Jika Anda mengontrol kode, Anda dapat melakukannya

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

dan ini akan menggunakan handler Anda untuk membuka koneksi.

Tapi sekali lagi, ini kurang memuaskan, karena Anda tidak perlu URL untuk melakukan ini - Anda ingin melakukan ini karena beberapa lib Anda tidak dapat (atau tidak ingin) mengendalikan keinginan url ...

Pendaftaran JVM Handler

Opsi terakhir adalah mendaftar URLStreamHandlerFactoryyang akan menangani semua url di 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);
    }
}

Untuk mendaftarkan pawang, hubungi URL.setURLStreamHandlerFactory()pabrik Anda yang sudah dikonfigurasi. Kemudian lakukan new URL("classpath:org/my/package/resource.extension")seperti contoh pertama dan Anda pergi.

Masalah Pendaftaran Handler JVM

Perhatikan bahwa metode ini hanya dapat dipanggil sekali per JVM, dan perhatikan juga bahwa Tomcat akan menggunakan metode ini untuk mendaftarkan penangan JNDI (AFAIK). Coba Jetty (saya akan); paling buruk, Anda dapat menggunakan metode ini terlebih dahulu dan kemudian harus bekerja di sekitar Anda!

Lisensi

Saya merilis ini ke domain publik, dan meminta jika Anda ingin memodifikasi bahwa Anda memulai proyek OSS di suatu tempat dan berkomentar di sini dengan detailnya. Implementasi yang lebih baik adalah memiliki URLStreamHandlerFactoryyang menggunakan ThreadLocals untuk menyimpan URLStreamHandlermasing-masing Thread.currentThread().getContextClassLoader(). Saya bahkan akan memberi Anda kelas modifikasi dan tes saya.

Stephen
sumber
1
@Stephen inilah yang saya cari. Bisakah Anda berbagi pembaruan dengan saya? Saya dapat memasukkan sebagai bagian dari com.github.fommil.common-utilspaket saya yang saya berencana untuk perbarui dan rilis segera melalui Sonatype.
fommil
5
Perhatikan bahwa Anda juga dapat menggunakan System.setProperty()untuk mendaftarkan protokol. SepertiSystem.setProperty("java.protocol.handler.pkgs", "org.my.protocols");
tsauerwein
Java 9+ memiliki metode yang lebih mudah: stackoverflow.com/a/56088592/511976
mhvelplund
100
URL url = getClass().getClassLoader().getResource("someresource.xxx");

Itu harus dilakukan.

Rasin
sumber
11
"Saya dapat melakukannya dengan menggunakan Class.getResourceAsStream (" a.xml "), tentu saja, tetapi itu akan mengharuskan saya untuk menggunakan API yang berbeda, dan karenanya mengubah kode yang ada. Saya ingin dapat menggunakan ini di semua tempat di mana Saya sudah bisa menentukan URL untuk sumber daya, dengan hanya memperbarui file properti. "
Thilo
3
-1 Seperti yang ditunjukkan oleh Thilo, ini adalah sesuatu yang dipertimbangkan dan ditolak OP.
sleske
13
getResource dan getResourceAsStream adalah metode yang berbeda. Setuju bahwa getResourceAsStream tidak cocok dengan API, tetapi getResource mengembalikan URL, yang persis seperti yang diminta OP.
romacafe
@romacafe: Ya, Anda benar. Ini adalah solusi alternatif yang bagus.
sleske
2
OP meminta solusi file properti, tetapi yang lain juga datang ke sini karena judul pertanyaan. Dan mereka menyukai solusi dinamis ini :)
Jarekczek
14

Saya pikir ini layak untuk dijawab sendiri - jika Anda menggunakan Spring, Anda sudah memilikinya

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

Seperti dijelaskan dalam dokumentasi musim semi dan ditunjukkan dalam komentar oleh skaffman.

eis
sumber
IMHO Spring ResourceLoader.getResource()lebih tepat untuk tugas ini ( ApplicationContext.getResource()delegasi di bawah tenda)
Lu55
11

Anda juga dapat mengatur properti secara terprogram saat startup:

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

Menggunakan kelas ini:

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

Dengan demikian Anda mendapatkan cara yang paling tidak mengganggu untuk melakukan ini. :) java.net.URL akan selalu menggunakan nilai saat ini dari properti sistem.

sub
sumber
1
Kode yang menambahkan paket tambahan untuk pencarian ke java.protocol.handler.pkgsvariabel sistem hanya dapat digunakan jika pawang bertujuan untuk memproses protokol yang belum "dikenal" seperti gopher://. Jika tujuannya adalah untuk mengesampingkan protokol "populer", suka file://atau http://, mungkin sudah terlambat untuk melakukannya, karena java.net.URL#handlerspeta sudah menambahkan penangan "standar" untuk protokol itu. Jadi satu-satunya jalan keluar adalah meneruskan variabel ini ke JVM.
dma_k
6

(Mirip dengan jawaban Azder , tetapi kebijaksanaan yang sedikit berbeda.)

Saya tidak percaya ada pengendali protokol yang telah ditentukan untuk konten dari classpath. ( classpath:Protokol yang disebut ).

Namun, Java memungkinkan Anda untuk menambahkan protokol Anda sendiri. Ini dilakukan dengan memberikan implementasi konkret java.net.URLStreamHandlerdan java.net.URLConnection.

Artikel ini menjelaskan bagaimana penangan aliran kustom dapat diimplementasikan: http://java.sun.com/developer/onlineTraining/protocolhandlers/ .

Dilum Ranatunga
sumber
4
Apakah Anda tahu daftar protokol apa yang dikirim dengan JVM?
Thilo
5

Saya telah membuat kelas yang membantu mengurangi kesalahan dalam mengatur penangan khusus dan memanfaatkan properti sistem sehingga tidak ada masalah dengan memanggil metode terlebih dahulu atau tidak berada di wadah yang tepat. Ada juga kelas pengecualian jika Anda salah:

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

Terinspirasi oleh @Stephen https://stackoverflow.com/a/1769454/980442 dan http://docstore.mik.ua/orelly/java/exp/ch09_06.htm

Menggunakan

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

buat saja kelas ini ke dalam sun.net.www.protocol.classpathpaket dan jalankan ke implementasi Oracle JVM agar bekerja seperti pesona.

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

Jika Anda menggunakan implementasi JVM lain, atur java.protocol.handler.pkgs=sun.net.www.protocolproperti sistem.

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

Daniel De León
sumber
3

Solusi dengan mendaftarkan URLStreamHandlers tentu saja paling benar, tetapi kadang-kadang diperlukan solusi paling sederhana. Jadi, saya menggunakan metode berikut untuk itu:

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

Dari Java 9+ dan lebih tinggi, Anda dapat mendefinisikan yang baru URLStreamHandlerProvider. The URLkelas menggunakan kerangka loader layanan untuk memuatnya pada jangka waktu.

Buat penyedia:

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

}

Buat file yang disebut java.net.spi.URLStreamHandlerProviderdi META-INF/servicesdirektori dengan konten:

org.example.ClasspathURLStreamHandlerProvider

Sekarang kelas URL akan menggunakan penyedia ketika melihat sesuatu seperti:

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

Saya tidak tahu apakah sudah ada, tetapi Anda dapat membuatnya sendiri dengan mudah.

Contoh protokol yang berbeda terlihat bagi saya seperti pola fasad. Anda memiliki antarmuka umum ketika ada implementasi yang berbeda untuk setiap kasus.

Anda bisa menggunakan prinsip yang sama, membuat kelas ResourceLoader yang mengambil string dari file properti Anda, dan memeriksa protokol khusus kami

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

strip myprotocol: dari awal string dan kemudian membuat keputusan cara memuat sumber daya, dan hanya memberi Anda sumber daya.

Azder
sumber
Ini tidak berfungsi jika Anda ingin lib pihak ketiga menggunakan URL dan Anda mungkin ingin menangani resolusi sumber daya untuk protokol tertentu.
mP.
2

Perpanjangan untuk jawaban Dilums :

Tanpa mengubah kode, Anda mungkin perlu mengejar implementasi khusus antarmuka terkait URL seperti yang disarankan Dilum. Untuk menyederhanakan hal-hal untuk Anda, saya dapat merekomendasikan melihat sumber untuk Sumber Daya Spring Framework . Walaupun kodenya tidak dalam bentuk stream handler, ia telah dirancang untuk melakukan apa yang Anda ingin lakukan dan berada di bawah lisensi ASL 2.0, membuatnya cukup ramah untuk digunakan kembali dalam kode Anda dengan kredit jatuh tempo.

DavidValeri
sumber
Halaman yang Anda referensi menyatakan bahwa "tidak ada implementasi standar URL yang dapat digunakan untuk mengakses sumber daya yang perlu diperoleh dari classpath, atau relatif ke ServletContext", yang menjawab pertanyaan saya, saya kira.
Thilo
@Homeless: bertahan di sana, anak muda. Dengan sedikit lebih banyak pengalaman, Anda akan segera memposting komentar dalam waktu singkat.
Cuga
1

Dalam aplikasi Spring Boot, saya menggunakan yang berikut ini untuk mendapatkan URL file,

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

Jika Anda memiliki kucing jantan di classpath, itu sesederhana:

TomcatURLStreamHandlerFactory.register();

Ini akan mendaftarkan penangan untuk protokol "perang" dan "classpath".

Olivier Gérardin
sumber
0

Saya mencoba menghindari URLkelas dan bukannya mengandalkan URI. Jadi untuk hal-hal yang perlu di URLmana saya ingin melakukan Spring Resource seperti mencari tanpa Spring saya melakukan hal berikut:

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

Untuk membuat URI bisa Anda gunakan URI.create(..). Cara ini juga lebih baik karena Anda mengontrol ClassLoaderyang akan melakukan pencarian sumber daya.

Saya perhatikan beberapa jawaban lain yang mencoba mengurai URL sebagai String untuk mendeteksi skema. Saya pikir lebih baik untuk menyebarkan URI dan menggunakannya untuk menguraikan.

Saya sebenarnya telah mengajukan masalah beberapa saat yang lalu dengan Spring Source memohon mereka untuk memisahkan kode Sumber Daya mereka coresehingga Anda tidak membutuhkan semua hal-hal Musim Semi lainnya.

Adam Gent
sumber