5000字,带你从根源深度了解SpringResource

Java和Spring在处理资源上的差异

虽然,Java为我们提供了Java.net.URL类和各种URL前缀的处理程序,Java.net.URL类是Java标准库中用于处理URL的类,它允许程序访问网络资源,如HTTP,FTP等协议,但是它们仍然在访问某些资源时存在不足,比如我们访问不是通过标准协议的资源,无法方便地从classpath(类路径)或在Servlet环境中相对于ServletContext获取资源,比如在Web应用中,可能需要访问/WEB-INF目录下的文件,比如在Tomcat中,通过SevcletContext.getResourceAsStream("/WEB-IF/web.xml"),这里的路径相当于Web应用的根目录,返回的可能是一个内部文件,无法用file:URL来表示。Java标准库中并没有提供现成的URL处理程序来处理这些情况,比如,当使用ClassLoader.getResource()方法的时候,返回的URL可能使用特定的协议,但实际上Java并没有为classpath协议内置URL处理程序,所以直接使用可能会出现问题。虽然我们可以注册新的URL前缀处理程序,Java允许通过URLStreamHandlerFactory来注册自定义的协议处理程序,比如自定义一个classpath协议,但实现这个过程需要编写URLStreamHandler和URLConnection的子类,并且在设置工厂时需要注意线程安全等问题,因为URL的工厂只能被设置一次。虽然我们可以注册新的URL前缀处理程序,但是实现复杂且URL接口缺少一些有用的功能,如检查资源是否存在的方法,因为在现有的URL类并没有直接的方法来判断资源是否存在,通常的做法都是尝试打开连接并捕获异常,但这并不是很直观。例如当我们调用openConnection()然后检查响应码或者尝试读取输入流,可能会引发IOExcetpion,需要处理异常,不如直接提供一个exists()方法方便。我们通过代码,可以直观的看出这些问题:

当我们需要检查资源是否存在时,在使用标准库的时候,我们可能是这样写的:

public static boolean exists(URL url)
{
try{
    HttpURLConnection connection  = (HttpURLConnection) url.openConnection();
    connection.setRequestMethod("HEAD");
    int responseCode = connection.getResponseCode();
    return (responseCode == HttpURLConnection.HTTP_OK);
}catch(IOException e)
{
    return false;
}
}

这种方法仅限于Http协议,对于其他的协议需要不同的处理方式,比如文件协议的话,我们需要检查文件是否存在:

File file = new File(url.toURL());
return file.exists();

但是这样又需要处理URL转换的异常,如果URL指向的不是文件系统资源(比如Jar包内的代码),这种方法也会失败。

比如我们注册一个自定义的URLStreamHandler的例子,需要创建一个URLStreamHandler的子类并覆盖openConnection方法,然后通过URL.setURLStreamHandlerFactory来设置工厂,这个工厂一旦被设置,就不能在更改了,这在多个库视图设置自己工厂的时候会出问题。

比如:

URL.setURLStreamHandlerFactory(protocol -> {
    if("ckasspath".equals(protocol))
    {
        return new ClasspathURLStreamHandler();
    }
    return null;
})
    class ClasspathURLStreamHandler extends URLStreamHandler{
        @Override
        protected URLConnection openConnection(URL u) throws IOException{
            String path = u.getPath;
            InputStream input = getClass().getClassLoader().getResourceAsStream(path);
            if(input == null)
            {
                throw new FileNotFoundException("Resource 没有找到"+path);
            }
            return new URLConnection(u)
            {
                @Override
                public void connect() throws IOException
                {
                return input;
                }
                
            }
        }
    }

这样的代码可以实现classpath:协议的处理,但必须确保JVM中仅设置一次工厂,否则抛出错误,对我们当下需要的灵活性应用不够友好。

还有一个问题是URL接口的功能缺少,在Java.net.URL中缺乏直观的方法来检查资源是否存在,我们只能通过尝试打开连接并捕获异常的方式间接实现,比如:

public boolean resourceExists(URL url)
{
    try{
        url.openConnection().connect()
            return true
    }catch(IOException e)
    {
        return false;
    }
}

这种方法效率低且不够优雅

因此,缺乏一个统一的exists()方法,导致我们需要针对不同的协议编写不同的代码。

然而,Spring框架中,使用Resource抽象(ClassPathResource,ServletContextResource)统一了资源访问模式,我们可以通过getResource("classoath:myfile.txt")来获取资源,然后调用exists(),isReadable()方法检查是否存在,这就完美解决了对标准URL不足的问题,Spring的 Resource抽象层解决了它。这是Resource接口的定义Code:

public interface Resource extends InputStreamSource {
​
    boolean exists();
​
    boolean isReadable();
​
    boolean isOpen();
​
    boolean isFile();
​
    URL getURL() throws IOException;
​
    URI getURI() throws IOException;
​
    File getFile() throws IOException;
​
    ReadableByteChannel readableChannel() throws IOException;
​
    long contentLength() throws IOException;
​
    long lastModified() throws IOException;
​
    Resource createRelative(String relativePath) throws IOException;
​
    String getFilename();
​
    String getDescription();
}

它扩展了InputStreamSource接口,我们来看看InputStreamSource接口的定义:

public interface InputStreamSource {
​
    InputStream getInputStream() throws IOException;
}

首先是getInputStream(),它的作用是定位并打开资源,返回用于读取资源的InputStream,它的核心其实是提供资源的原始字节流,每次调用都应该返回一个新的InputStream,适用于读取文件内容,类路径资源,网络资源等,因为关闭该流是调用者的责任,比如:

Resource resource = new ClassPathResource("config.xml");
try(InputStream input = resource.getInputStream())
{
    byte[] data = input.readAllBytes();
}catch(IOException e)
{
    
}

然后是exists()方法,它的作用是返回一个boolean值,表示这个资源是否以物理形式实际存在,即快速检查资源是否存在,不需要打开流,那么它使用于读取资源前进行预校验,比如检查配置文件是否存在在决定是否加载默认配置,比如:

if(resource.exists())
{
    try(InputStream input = resource.getInputStream()){}
       
}else{
    使用默认配置或者抛出异常
}

然后是isOpen()方法,它会返回一个boolean,表示该资源是否存在一个已经开放流的句柄,如果为true,则InputStream不能被多次读取,必须只读一次并关闭,避免资源泄露。对于所有常规资源实现(除了InputStreamResourece外),通常返回false,它的核心功能是表示资源是否关联到一个已经打开的流,仅当资源直接包装了InputStream(如InputStreamResource)返回true。此时流只能读取一次,且必须由调用者关闭。而false的情况下是绝大多数资源(如ClassPathResource,FileSystemResource)返回false,表示每次调用getInputStream()都会生成新流,可多次安全调用,比如:

Resource resource = new InputStreamResource(rawInputStream)
{
    if(Resource.isOpen())
    {
    //该资源直接包装一个打开的流
    try(InputStream input = resource.getInputStream())
    {
    //只能读一次斌且关闭
    }
    }
}

然后是getDescription(),它会返回该资源的描述,常用于处理资源时的错误输入,通常是全路径文件名或实际URL,它的核心功能是提供资源的可读标识,用于日志,异常信息等调试场景,比如ClassPathResource: class path resource[config.xml],FileSystemResource : file[/app/config.xml],UrlResource: URl[http://demo/data.json],代码如下:

try {
    resource.getInputStream();
} catch (FileNotFoundException e) {
    logger.error("资源未找到: {}", resource.getDescription());
    // 输出:资源未找到: class path resource [config.xml]
}

其他方法就是获取底层URl或File对象,如getURL(),返回资源的URL对象,getFile(),返回资源的FIle对象

总之,优先使用getInputStream(),这是最安全,最通用的方法,适用于所有资源类型(包括类路径,网络,文件系统等)

仅在确定资源是本地文件系统路径的时候使用getFile(),否则还是用getInputStream替代,然后要学会使用exists()方法替代不必要的异常处理,多关注isOpen的返回值,必须确保流只读取一次就及时关闭,然后出现问题了使用getDescription()调试,根据日志记录资源描述。快速定位问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值