URL เพื่อโหลดทรัพยากรจาก classpath ใน Java


197

ใน 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 เป็นเพียงสตริงการโหลดทรัพยากรจึงสามารถกำหนดค่าได้อย่างง่ายดาย

มีโปรโตคอลในการโหลดทรัพยากรโดยใช้ classloader ปัจจุบันหรือไม่? นี่คล้ายกับโปรโตคอล Jar ยกเว้นว่าฉันไม่จำเป็นต้องรู้ว่าไฟล์ jar หรือโฟลเดอร์คลาสใดที่ทรัพยากรมาจาก

แน่นอนว่าฉันสามารถทำได้โดยใช้Class.getResourceAsStream("a.xml")แต่จะต้องใช้ API อื่นและด้วยเหตุนี้การเปลี่ยนแปลงรหัสที่มีอยู่ ฉันต้องการที่จะสามารถใช้สิ่งนี้ในทุกสถานที่ที่ฉันสามารถระบุ URL สำหรับทรัพยากรได้โดยเพียงแค่อัปเดตไฟล์คุณสมบัติ

คำตอบ:


348

บทนำและการนำไปปฏิบัติขั้นพื้นฐาน

ก่อนอื่นคุณต้องมี 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 - ซึ่งเป็นเหตุผลที่ฉันต้องการทั้งหมดนี้ )

วิธีการแก้ปัญหา / การเพิ่มประสิทธิภาพ

ข้อมูลจำเพาะ Handler รหัสคู่มือ

หากคุณควบคุมรหัสคุณสามารถทำได้

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

และสิ่งนี้จะใช้ตัวจัดการของคุณเพื่อเปิดการเชื่อมต่อ

แต่อีกครั้งสิ่งนี้น่าพอใจน้อยกว่าเพราะคุณไม่จำเป็นต้องใช้ URL ในการทำเช่นนี้ - คุณต้องการทำเช่นนี้เพราะ lib บางอย่างที่คุณไม่สามารถควบคุม (หรือไม่ต้องการ) ต้องการ URL ...

การลงทะเบียน JVM Handler

ตัวเลือกที่ดีที่สุดคือการลงทะเบียน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 Handler

โปรดทราบว่าวิธีนี้อาจเรียกได้เพียงครั้งเดียวต่อ JVM และโปรดทราบว่า Tomcat จะใช้วิธีนี้ในการลงทะเบียน JNDI handler (AFAIK) ลองท่าเทียบเรือ (ฉันจะ) ที่เลวร้ายที่สุดคุณสามารถใช้วิธีการก่อนแล้วจึงต้องแก้ไขคุณ!

การอนุญาต

ฉันเผยแพร่สิ่งนี้เป็นสาธารณสมบัติและถามว่าถ้าคุณต้องการแก้ไขว่าคุณเริ่มโครงการ OSS ที่ไหนสักแห่งและแสดงความคิดเห็นที่นี่พร้อมรายละเอียด การดำเนินที่ดีกว่าจะมีURLStreamHandlerFactoryที่ใช้ThreadLocalเพื่อการจัดเก็บสำหรับแต่ละURLStreamHandler Thread.currentThread().getContextClassLoader()ฉันจะให้การดัดแปลงและการทดสอบแก่คุณ


1
@ สตีเฟ่นนี่คือสิ่งที่ฉันกำลังมองหา คุณช่วยแบ่งปันการอัพเดทของฉันกับฉันได้ไหม ฉันสามารถรวมเป็นส่วนหนึ่งของcom.github.fommil.common-utilsแพ็คเกจที่ฉันวางแผนจะอัปเดตและวางจำหน่ายในไม่ช้าผ่าน Sonatype
fommil

5
โปรดทราบว่าคุณยังสามารถใช้System.setProperty()เพื่อลงทะเบียนโปรโตคอล ชอบSystem.setProperty("java.protocol.handler.pkgs", "org.my.protocols");
tsauerwein

Java 9+ มีวิธีที่ง่ายกว่า: stackoverflow.com/a/56088592/511976
mhvelplund

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

ที่ควรทำ


11
"ฉันสามารถทำได้โดยใช้ Class.getResourceAsStream (" a.xml ") แน่นอน แต่นั่นจะทำให้ฉันต้องใช้ API ที่แตกต่างกันและด้วยเหตุนี้การเปลี่ยนแปลงรหัสที่มีอยู่ฉันต้องการที่จะใช้มันในทุกที่ ฉันสามารถระบุ URL สำหรับทรัพยากรได้แล้วโดยเพียงอัปเดตไฟล์คุณสมบัติ "
Thilo

3
-1 ดังที่ Thilo ชี้ให้เห็นนี่เป็นสิ่งที่ OP พิจารณาและปฏิเสธ
sleske

13
getResource และ getResourceAsStream เป็นวิธีการที่แตกต่างกัน ยอมรับว่า getResourceAsStream ไม่สอดคล้องกับ API แต่ getResource จะส่งคืน URL ซึ่งเป็นสิ่งที่ OP ต้องการ
romacafe

@romacafe: ใช่คุณพูดถูก นี่เป็นทางเลือกที่ดี
sleske

2
OP ขอวิธีแก้ไขปัญหาไฟล์คุณสมบัติ แต่คนอื่น ๆ ก็มาที่นี่เพราะชื่อคำถาม และพวกเขาต้องการการแก้ปัญหาแบบไดนามิกนี้ :)
Jarekczek

14

ฉันคิดว่านี่เป็นคำตอบที่คุ้มค่าถ้าคุณใช้ Spring คุณมีสิ่งนี้อยู่แล้ว

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

เช่นเดียวกับที่อธิบายไว้ในเอกสารฤดูใบไม้ผลิและชี้ให้เห็นในความคิดเห็นโดย skaffman


IMHO Spring ResourceLoader.getResource()เหมาะสมกว่าสำหรับงาน ( ApplicationContext.getResource()มอบหมายให้ภายใต้ประทุน)
Lu55

11

คุณยังสามารถตั้งค่าคุณสมบัติโดยทางโปรแกรมในระหว่างการเริ่มต้น:

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 จะใช้ค่าปัจจุบันจากคุณสมบัติของระบบเสมอ


1
รหัสซึ่งจะเพิ่มแพคเกจพิเศษสำหรับการค้นหาเพื่อjava.protocol.handler.pkgsตัวแปรระบบสามารถใช้ได้เฉพาะถ้าจัดการที่มุ่งที่จะดำเนินการไม่ได้ "รู้จักกันดี" gopher://โปรโตคอลเช่น หากความตั้งใจที่จะแทนที่โปรโตคอล "ยอดนิยม" เช่นfile://หรือhttp://อาจจะสายเกินไปที่จะทำเพราะjava.net.URL#handlersแผนที่ได้เพิ่มตัวจัดการ "มาตรฐาน" สำหรับโปรโตคอลนั้นแล้ว ทางออกเดียวคือส่งผ่านตัวแปรนี้ไปยัง JVM
dma_k

6

(คล้ายกับคำตอบของ Azderแต่มีชั้นเชิงที่แตกต่างออกไปเล็กน้อย)

ฉันไม่เชื่อว่ามีตัวจัดการโปรโตคอลที่กำหนดไว้ล่วงหน้าสำหรับเนื้อหาจาก classpath ( classpath:โปรโตคอลที่เรียกว่า)

อย่างไรก็ตาม Java อนุญาตให้คุณเพิ่มโปรโตคอลของคุณเอง นี้จะกระทำผ่านการใช้งานให้เป็นรูปธรรมและjava.net.URLStreamHandlerjava.net.URLConnection

บทความนี้อธิบายวิธีการใช้ตัวจัดการกระแสข้อมูลที่กำหนดเองสามารถนำไปใช้: http://java.sun.com/developer/onlineTraining/protocolhandlers/


4
คุณรู้หรือไม่ว่ามีโปรโตคอลใดที่มาพร้อมกับ JVM
Thilo

5

ฉันได้สร้างคลาสที่ช่วยลดข้อผิดพลาดในการตั้งค่าตัวจัดการที่กำหนดเองและใช้ประโยชน์จากคุณสมบัติของระบบดังนั้นจึงไม่มีปัญหากับการเรียกใช้เมธอดก่อนหรือไม่ได้อยู่ในคอนเทนเนอร์ที่เหมาะสม นอกจากนี้ยังมีคลาสยกเว้นถ้าคุณทำสิ่งผิดปกติ:

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

}

5

สร้างแรงบันดาลใจโดย @Stephen https://stackoverflow.com/a/1769454/980442 และ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คุณสมบัติระบบ

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


3

แน่นอนว่าโซลูชันที่มีการลงทะเบียน 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;
}

3

จาก 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");

2

ฉันไม่รู้ว่ามีอยู่แล้ว แต่คุณสามารถทำให้มันง่ายขึ้น

ตัวอย่างโปรโตคอลที่แตกต่างนั้นดูเหมือนกับฉันเหมือนลวดลายด้านหน้า คุณมีอินเตอร์เฟสทั่วไปเมื่อมีการนำไปใช้งานที่แตกต่างกันสำหรับแต่ละเคส

คุณสามารถใช้หลักการเดียวกันสร้างคลาส 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: จากจุดเริ่มต้นของสตริงจากนั้นทำการตัดสินใจว่าจะโหลดทรัพยากรได้อย่างไรและให้ทรัพยากรแก่คุณ


วิธีนี้ใช้ไม่ได้หากคุณต้องการให้ lib บุคคลที่สามใช้ URL และคุณอาจต้องการจัดการความละเอียดของทรัพยากรสำหรับโปรโตคอลเฉพาะ
mP

2

ส่วนขยายของคำตอบของ Dilums :

หากไม่มีการเปลี่ยนรหัสคุณอาจต้องมีการติดตั้งอินเตอร์เฟสที่เกี่ยวข้องกับ URL ตามที่ Dilum แนะนำ เพื่อลดความซับซ้อนของสิ่งที่คุณจะสามารถขอแนะนำให้ดูแหล่งสำหรับทรัพยากรฤดูใบไม้ผลิของกรอบ ในขณะที่รหัสไม่ได้อยู่ในรูปแบบของตัวจัดการกระแสข้อมูล แต่ได้รับการออกแบบมาเพื่อทำสิ่งที่คุณต้องการและอยู่ภายใต้ใบอนุญาต ASL 2.0 ทำให้เป็นมิตรเพียงพอสำหรับการใช้งานซ้ำในรหัสของคุณด้วยเครดิตที่กำหนด


หน้านั้นคุณอ้างอิงระบุว่า "ไม่มีการใช้ URL มาตรฐานที่อาจใช้เพื่อเข้าถึงทรัพยากรที่ต้องได้รับจาก classpath หรือเกี่ยวข้องกับ ServletContext" ซึ่งตอบคำถามของฉันฉันเดา
Thilo

@Homeless: แขวนไว้ที่นั่นชายหนุ่ม ด้วยประสบการณ์ที่เพิ่มขึ้นเล็กน้อยคุณจะสามารถโพสต์ความคิดเห็นได้ในเวลาไม่นาน
Cuga

1

ในแอพ Spring Boot ฉันใช้สิ่งต่อไปนี้เพื่อรับ URL ไฟล์

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

1

หากคุณมีแมวตัวผู้ที่ classpath มันง่ายเหมือน:

TomcatURLStreamHandlerFactory.register();

สิ่งนี้จะลงทะเบียนตัวจัดการสำหรับโปรโตคอล "war" และ "classpath"


0

ฉันพยายามที่จะหลีกเลี่ยงการเรียนและแทนที่จะพึ่งพา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 ทั้งหมด

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.