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()调试,根据日志记录资源描述。快速定位问题