URL để tải tài nguyên từ đường dẫn lớp trong Java


197

Trong Java, bạn có thể tải tất cả các loại tài nguyên bằng cùng một API nhưng với các giao thức URL khác nhau:

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

Điều này sẽ tách riêng việc tải tài nguyên thực tế khỏi ứng dụng cần tài nguyên và vì URL chỉ là một Chuỗi, nên việc tải tài nguyên cũng rất dễ cấu hình.

Có một giao thức để tải tài nguyên bằng cách sử dụng trình nạp lớp hiện tại không? Điều này tương tự với giao thức Jar, ngoại trừ việc tôi không cần biết tệp jar hoặc thư mục lớp mà tài nguyên đến từ đâu.

Tôi có thể làm điều đó bằng cách sử dụng Class.getResourceAsStream("a.xml"), tất nhiên, nhưng điều đó sẽ yêu cầu tôi sử dụng một API khác và do đó thay đổi mã hiện có. Tôi muốn có thể sử dụng điều này ở tất cả những nơi tôi có thể chỉ định URL cho tài nguyên đã có, chỉ bằng cách cập nhật tệp thuộc tính.

Câu trả lời:


348

Giới thiệu và thực hiện cơ bản

Trước tiên, bạn sẽ cần ít nhất một URLStreamHandler. Điều này thực sự sẽ mở kết nối đến một URL nhất định. Lưu ý rằng điều này được gọi đơn giản Handler; điều này cho phép bạn chỉ định java -Djava.protocol.handler.pkgs=org.my.protocolsvà nó sẽ tự động được chọn, sử dụng tên gói "đơn giản" làm giao thức được hỗ trợ (trong trường hợp này là "classpath").

Sử dụng

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

Vấn đề ra mắt

Nếu bạn là bất cứ ai như tôi, bạn không muốn dựa vào một thuộc tính được thiết lập trong lần khởi chạy để đưa bạn đến một nơi nào đó (trong trường hợp của tôi, tôi muốn giữ các tùy chọn của mình mở như Java WebStart - đó là lý do tại sao tôi cần tất cả điều này ).

Giải pháp thay thế / cải tiến

Hướng dẫn sử dụng mã đặc tả

Nếu bạn kiểm soát mã, bạn có thể làm

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

và điều này sẽ sử dụng trình xử lý của bạn để mở kết nối.

Nhưng một lần nữa, điều này không thỏa đáng, vì bạn không cần URL để làm điều này - bạn muốn làm điều này bởi vì một số lib bạn không thể (hoặc không muốn) kiểm soát muốn url ...

Đăng ký xử lý JVM

Tùy chọn cuối cùng là đăng ký một URLStreamHandlerFactorycái sẽ xử lý tất cả các url trên 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);
    }
}

Để đăng ký xử lý, hãy gọi URL.setURLStreamHandlerFactory()với nhà máy được cấu hình của bạn. Sau đó, làm new URL("classpath:org/my/package/resource.extension")như ví dụ đầu tiên và đi bạn đi.

Vấn đề đăng ký xử lý JVM

Lưu ý rằng phương thức này chỉ có thể được gọi một lần cho mỗi JVM và lưu ý rằng Tomcat sẽ sử dụng phương thức này để đăng ký trình xử lý JNDI (AFAIK). Hãy thử cầu tàu (tôi sẽ được); tệ nhất, bạn có thể sử dụng phương pháp này trước và sau đó nó phải hoạt động xung quanh bạn!

Giấy phép

Tôi phát hành nó cho miền công cộng và hỏi rằng nếu bạn muốn sửa đổi rằng bạn bắt đầu một dự án OSS ở đâu đó và bình luận ở đây với các chi tiết. Việc thực hiện tốt hơn sẽ là có một URLStreamHandlerFactorysử dụng ThreadLocals để lưu trữ URLStreamHandlers cho mỗi Thread.currentThread().getContextClassLoader(). Tôi thậm chí sẽ cung cấp cho bạn các sửa đổi và các lớp kiểm tra của tôi.


1
@Stephen đây chính xác là những gì tôi đang tìm kiếm. Bạn có thể vui lòng chia sẻ thông tin cập nhật của bạn với tôi? Tôi có thể đưa vào như một phần của com.github.fommil.common-utilsgói mà tôi dự định sẽ cập nhật và phát hành sớm thông qua Sonatype.
fommil

5
Lưu ý rằng bạn cũng có thể sử dụng System.setProperty()để đăng ký giao thức. Giống nhưSystem.setProperty("java.protocol.handler.pkgs", "org.my.protocols");
tsauerwein

Java 9+ có một phương thức dễ dàng hơn: stackoverflow.com/a/56088592/511976
mhvelplund

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

Nên làm vậy.


11
"Tôi có thể làm điều đó bằng cách sử dụng Class.getResourceAsStream (" a.xml "), nhưng điều đó sẽ yêu cầu tôi sử dụng một API khác và do đó thay đổi thành mã hiện có. Tôi muốn có thể sử dụng điều này ở mọi nơi Tôi có thể chỉ định một URL cho tài nguyên đã có, chỉ bằng cách cập nhật tệp thuộc tính. "
Thilo

3
-1 Như Thilo đã chỉ ra, đây là điều mà OP đã cân nhắc và từ chối.
sleske

13
getResource và getResourceAsStream là các phương thức khác nhau. Đồng ý rằng getResourceAsStream không phù hợp với API, nhưng getResource trả về một URL, đó chính xác là những gì OP yêu cầu.
romacafe

@romacafe: Vâng, bạn nói đúng. Đây là một giải pháp thay thế tốt.
sleske

2
OP yêu cầu giải pháp tập tin tài sản, nhưng những người khác cũng đến đây vì tiêu đề của câu hỏi. Và họ thích giải pháp năng động này :)
Jarekczek

14

Tôi nghĩ rằng đây là giá trị câu trả lời của riêng nó - nếu bạn đang sử dụng Spring, bạn đã có điều này với

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

Giống như được giải thích trong tài liệu mùa xuân và chỉ ra trong các ý kiến ​​của skaffman.


IMHO Spring ResourceLoader.getResource()thích hợp hơn cho nhiệm vụ ( ApplicationContext.getResource()đại biểu cho nó dưới mui xe)
Lu55

11

Bạn cũng có thể thiết lập thuộc tính theo chương trình trong khi khởi động:

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

Sử dụng lớp này:

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

Vì vậy, bạn có được cách xâm nhập ít nhất để làm điều này. :) java.net.URL sẽ luôn sử dụng giá trị hiện tại từ các thuộc tính hệ thống.


1
Mã chỉ thêm gói bổ sung để tra cứu java.protocol.handler.pkgsbiến hệ thống chỉ có thể được sử dụng nếu trình xử lý nhằm xử lý giao thức chưa được biết đến như là gopher://. Nếu ý định là ghi đè giao thức "phổ biến", như file://hoặc http://, có thể đã quá muộn để thực hiện, vì java.net.URL#handlersbản đồ đã được thêm một trình xử lý "tiêu chuẩn" cho giao thức đó. Vì vậy, cách duy nhất là chuyển biến này sang JVM.
dma_k

6

(Tương tự như câu trả lời của Azder , nhưng một chiến thuật hơi khác.)

Tôi không tin rằng có một trình xử lý giao thức được xác định trước cho nội dung từ đường dẫn lớp. (Cái gọi là classpath:giao thức).

Tuy nhiên, Java cho phép bạn thêm các giao thức của riêng bạn. Điều này được thực hiện thông qua việc cung cấp các triển khai cụ thể java.net.URLStreamHandlerjava.net.URLConnection.

Bài viết này mô tả cách xử lý luồng tùy chỉnh có thể được thực hiện: http://java.sun.com/developer/onlineTraining/protatiohandlers/ .


4
Bạn có biết danh sách các giao thức nào được gửi với JVM không?
Thilo

5

Tôi đã tạo một lớp giúp giảm lỗi trong việc thiết lập trình xử lý tùy chỉnh và tận dụng thuộc tính hệ thống để không gặp vấn đề gì khi gọi phương thức trước hoặc không nằm trong vùng chứa đúng. Ngoài ra còn có một lớp ngoại lệ nếu bạn gặp sự cố:

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

Truyền cảm hứng bởi @Stephen https://stackoverflow.com/a/1769454/980442http://docstore.mik.ua/orelly/java/api/ch09_06.htmlm

Để sử dụng

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

chỉ cần tạo lớp này thành sun.net.www.protocol.classpathgói và chạy nó vào triển khai Oracle JVM để hoạt động như một bùa mê.

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

Trong trường hợp bạn đang sử dụng một triển khai JVM khác, đặt thuộc tính java.protocol.handler.pkgs=sun.net.www.protocolhệ thống.

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


3

Tất nhiên, giải pháp với việc đăng ký URLStreamHandlers là chính xác nhất, nhưng đôi khi giải pháp đơn giản nhất là cần thiết. Vì vậy, tôi sử dụng phương pháp sau cho điều đó:

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

Từ Java 9+ trở lên, bạn có thể định nghĩa một cái mới URLStreamHandlerProvider. Các URLlớp học sử dụng khuôn khổ loader dịch vụ để tải nó tại thời gian chạy.

Tạo một nhà cung cấp:

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

}

Tạo một tệp được gọi java.net.spi.URLStreamHandlerProvidertrong META-INF/servicesthư mục với nội dung:

org.example.ClasspathURLStreamHandlerProvider

Bây giờ lớp URL sẽ sử dụng nhà cung cấp khi thấy một cái gì đó như:

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

2

Tôi không biết nếu đã có một cái rồi, nhưng bạn có thể tự làm nó dễ dàng hơn.

Ví dụ về các giao thức khác nhau trông giống như một mẫu mặt tiền. Bạn có một giao diện chung khi có các triển khai khác nhau cho từng trường hợp.

Bạn có thể sử dụng cùng một nguyên tắc, tạo một lớp ResourceLoader lấy chuỗi từ tệp thuộc tính của bạn và kiểm tra giao thức tùy chỉnh của chúng tôi

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

loại bỏ myprotatio: từ đầu chuỗi và sau đó đưa ra quyết định về cách tải tài nguyên và chỉ cung cấp cho bạn tài nguyên.


Điều này không hoạt động nếu bạn muốn lib của bên thứ ba sử dụng URL và có lẽ bạn muốn xử lý độ phân giải tài nguyên cho một giao thức cụ thể.
mP.

2

Một phần mở rộng cho câu trả lời của Dilums :

Không thay đổi mã, bạn có thể cần theo đuổi việc triển khai tùy chỉnh các giao diện liên quan đến URL như Dilum khuyến nghị. Để đơn giản hóa mọi thứ cho bạn, tôi có thể khuyên bạn nên xem nguồn cho Tài nguyên của Spring Framework . Mặc dù mã không ở dạng xử lý luồng, nó được thiết kế để thực hiện chính xác những gì bạn đang muốn làm và theo giấy phép ASL 2.0, làm cho nó đủ thân thiện để sử dụng lại mã của bạn với tín dụng đáo hạn.


Trang mà bạn tham chiếu nói rằng "không có triển khai URL được tiêu chuẩn hóa có thể được sử dụng để truy cập tài nguyên cần lấy từ đường dẫn lớp hoặc liên quan đến ServletContext", tôi trả lời câu hỏi của tôi.
Thilo

@Homless: treo ở đó, chàng trai. Với một chút kinh nghiệm, bạn sẽ sớm đăng bình luận ngay lập tức.
Cuga

1

Trong ứng dụng Spring Boot, tôi đã sử dụng cách sau để nhận URL tệp,

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

1

Nếu bạn có tomcat trên classpath, nó đơn giản như:

TomcatURLStreamHandlerFactory.register();

Điều này sẽ đăng ký xử lý cho các giao thức "chiến tranh" và "classpath".


0

Tôi cố gắng tránh URLlớp và thay vào đó dựa vào URI. Vì vậy, đối với những thứ cần URLnơi tôi muốn làm Tài nguyên mùa xuân như tra cứu với Mùa xuân, tôi làm như sau:

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

Để tạo một URI bạn có thể sử dụng URI.create(..). Cách này cũng tốt hơn bởi vì bạn kiểm soát việc ClassLoaderđó sẽ thực hiện tra cứu tài nguyên.

Tôi nhận thấy một số câu trả lời khác đang cố phân tích URL dưới dạng Chuỗi để phát hiện lược đồ. Tôi nghĩ tốt hơn là vượt qua URI và sử dụng nó để phân tích cú pháp thay thế.

Tôi thực sự đã gửi một vấn đề trước đây với Spring Source cầu xin họ tách mã Tài nguyên của họ ra coređể bạn không cần tất cả các công cụ Spring khác.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.