当前位置: 代码迷 >> 综合 >> Spring5:就这一次,搞定资源加载器之资源抽象接口Resource
  详细解决方案

Spring5:就这一次,搞定资源加载器之资源抽象接口Resource

热度:45   发布时间:2024-01-16 14:13:41.0

在日常的程序开发的过程中,咱们经常要做的一件事就是加载资源。JDK为我们提供了File,URL等类供咱们使用。然而,在java程序中,资源的存放位置是各异的,有的存放在classpath中,有的存放在文件系统中,有的存放在web应用中。而对于不同位置的资源,java程序获取这些资源的方法各有不同。 

对于classpath:咱们通常使用类加载器加载资源文件,例如:

Class.getResource(String path) //方式一
Class.getClassLoader().getResource(String path)//方式二

方式一,可以加载当前类所在包下或其子包下的的资源,如果参数已“/”开头,则从classpath下加载配置文件。

方式二,可以加载classpath下或其子包下的资源,只能接收相对路径,如果以“/”开头,会返回null,原因是,ClassLoader.getResource(String path)使用双亲委托加载机制来加载资源,咱们知道,java有三种类加载器:

1)引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。它负责将<Java_Runtime_Home>/lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作

2)扩展类加载器(extensions class loader):该类加载器在此目录里面查找并加载 Java 类。扩展类加载器是由Sun的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量-Djava.ext.dirs指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

3)系统统类加载器(System class loader)系统类加载器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

那么咱们自定义的资源一般会扔在classpath下,所以肯定会由AppClassLoader加载,AppClassLoader不会直接加载,而是委托给ExtClassLoader,ExtClassLoader也不会直接加载,它同样会委托给引导类加载器,这时,咱们看一下源代码:

    public URL getResource(String name) {URL url;if (parent != null) {   //1url = parent.getResource(name);} else {url = getBootstrapResource(name);//2}if (url == null) {url = findResource(name); //3}return url;}

当 ExtClassLoader委托给引导类加载器时,parent就会等于null,因为引导类加载器是一段C++ 程序,java无法引用,程序会走到第二步,

    /*** Find resources from the VM's built-in classloader.*/private static URL getBootstrapResource(String name) {URLClassPath ucp = getBootstrapClassPath();Resource res = ucp.getResource(name);return res != null ? res.getURL() : null;}

getBootstrapResource(String name)显然是从引<Java_Runtime_Home>/lib加载资源,很显然,咱们的资源文件并不在核心包下,所以步骤2肯定返回的是null,这时程序走到步骤3:

   protected URL findResource(String name) {return null;}

findResource(String name)是个空方法,该方法是提供给自定义类加载器重写用的,作用是定义自定义类加载器加载资源的算法,这里直接返回null,所以最后ClassLoader.getResource(String path)方法返回的就是null了。那为什么class.getResource(String name)方法就可以用“/”开头呢?是因为:

   public java.net.URL getResource(String name) {name = resolveName(name);ClassLoader cl = getClassLoader0();if (cl==null) {// A system class.return ClassLoader.getSystemResource(name);}return cl.getResource(name);}
Class.getResource(String name),不使用双亲委托加载机制,而是直接使用AppClassLoader,咱们知道AppClassLoader加载的是classpath下的资源,所以它可以加载以“/”开头的资源。

咱们也可以这么写:

Class.getResourceAsStream(String name) //方式三
ClassLoader.getResourceAsStream(String name)//方式四

这两种写法和上面的两种效果差不多,方式三可以加载当前class所在包及其子包下的资源,和方式一的区别在于,方式一直接返回URL,而方式三会在URL的基础上返回一个InputStream,源码:

    public InputStream getResourceAsStream(String name) {URL url = getResource(name);try {return url != null ? url.openStream() : null;} catch (IOException e) {return null;}}

方式四和方式二效果是一样的,同样为调用者返回一个InputStream。咱们可以发现,不管用Class,还是ClassLoader,其实最终都是由ClassLoader来加载的,只不过,Class在ClasssLoader的基础上进行了增强,即可以加载调用者class所在包或其子包下或者classpath下的资源,class.getResource(String name)源码:

  public java.net.URL getResource(String name) {name = resolveName(name);  //1ClassLoader cl = getClassLoader0();if (cl==null) {// A system class.return ClassLoader.getSystemResource(name);}return cl.getResource(name);//2}

代码一处调用名为resolveName的函数,咱们打开这个函数:

    private String resolveName(String name) {if (name == null) {return name;}if (!name.startsWith("/")) {Class<?> c = this;while (c.isArray()) {c = c.getComponentType();}String baseName = c.getName();int index = baseName.lastIndexOf('.');if (index != -1) {name = baseName.substring(0, index).replace('.', '/')+"/"+name;}} else {name = name.substring(1);}return name;}

该方法的作用是是把包路径转换成文件路径,然后再拼接上要加载的资源名,如果调用该方法传入一个以“/”开头的路径,那么“斜杠”会被截取掉,从而得到一个相对路径,这个路径相对于classpath。 

代码二处调用了ClassLoader.getResource()方法,完成资源加载,咱们可以认为Class.getResource(String path)是ClassLoader.getResource(String path)方法的扩展。

对于文件系统,咱们可以构造以绝对路径为参数的file和以相对路径为参数的file来加载资源,绝对路径自然不必多说,咱们只需要传一个文件的绝对路径给File就可以了,像这样:

   File file = new File("D:/hust/file.txt");

那么对于相对路径来说,就要搞清楚File的相对路径相对的是谁?其实,File的相对路径相对的是虚拟机的启动路径,也有叫工作目录的,其实都是一个意思,也就是user.dir的属性属性值。对于普通的java程序来讲,main函数所在的class所在的包就是工作目录,对于web程序来讲,要看具体的web容器,比如tomcat就是bin目录,而maven+jetty插件的形式user.dir属性值返回的就是执行maven命令的目录。所以这里面就有坑了,也就是说,user.dir在不同的环境下会返回不同的值,如果在程序中硬编码资源文件的路径,是会出问题的。

对于java web项目,也就是Servlet,咱们一般用 getRealPath方法:

File file = new File(request.getSession().getServletContext().getRealPath("WEB-INF/classes/test.properties"));

当然,ServletContext也提供了getResourceAsStream方法:

InputStream in = request.getSession().getServletContext().getResourceAsStream("WEB-INF/classes/test.properties");

ServletContext也提供getResource(String name)方法:

URL url = request.getSession().getServletContext().getResource("WEB-INF/classes/test.properties");

咱们分析到这里,发现JDK针对不同的环境提供了不同的加载资源方式,但是这些资源加载方式却没有统一的接口供开发者调用,这样在开发的过程中开发者不得不为这些小的细节费些功夫,有时稍有不慎会误入歧途,甚至有些经验不足的开发者根本不了解这些方法的细节和原理,拿来照用,一不小心就出错了。

Spring 在其现有的基础上抽象出了统一的资源接口,并为不同环境,不同方式一一提供了实现,最让人赏心悦目的是,Spring通过底层封装, 把不同环境, 不同来源的资源加载细节封装到了底层的算法当中,开发者不用关心底层实现 细节,只需关注资源路径表达式即可。举例,加载classpath下的资源,可以这样写:

classpath:config.xml 

也可以这样写:config.xml

也可以这样写:/WEB-INF/classes/config.xml

还可以用ant风格的表达是批量加载资源文件。

下面在那么就来分析一下Spring 为开发者提供的统一资源抽象接口Resource

public interface Resource extends InputStreamSource {boolean exists();URL getURL() throws IOException;URI getURI() throws IOException;File getFile() throws IOException;long contentLength() throws IOException;long lastModified() throws IOException;Resource createRelative(String relativePath) throws IOException;@NullableString getFilename();String getDescription();}

这些方法的作用,咱们通过名字就能明白其功能,这里就不分析了,咱们就分析一下几个常用的实现类:

ClassPathResoure: 代表类路径下的资源,资源以相对类路径的方式表示 (类路径是指类文件存放的路径,也就是classpath),它使用线程上下文加载器,或者给定的类加载器(自定义的类加载器或其他)或者给定的类的类加载器来加载资源。其实就是封装了咱们上面分析过的利用classLoader加载资源文件的方法,源码:

	public InputStream getInputStream() throws IOException {InputStream is;if (this.clazz != null) {is = this.clazz.getResourceAsStream(this.path);}else if (this.classLoader != null) {is = this.classLoader.getResourceAsStream(this.path);}else {is = ClassLoader.getSystemResourceAsStream(this.path);}if (is == null) {throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");}return is;}

getInputSteam 等于是class.getResourceAsStream和classLoader.getResourceAsStream的结合体。

FileSystemResouce: 代表文件系统资源,资源以文件系统路径方式表示,也就是说,FileSystemResource是用于处理java.io.File的实现,它同时能解析作为file和作为url的资源(file 和url 是可以相互转换的),实现如下:

	public URL getURL() throws IOException {return this.file.toURI().toURL();}
	public InputStream getInputStream() throws IOException {return Files.newInputStream(this.file.toPath());}
	public File getFile() {return this.file;}

ServletContextResource: 为获取Web根路径的ServeltContext资源而提供的Resource实现,它支持以流和URL的方式访问,其实就是封装了咱们在上面分析的servlet加载资源的三种方式,咱们可以看一下其实现代码:

	public InputStream getInputStream() throws IOException {InputStream is = this.servletContext.getResourceAsStream(this.path);if (is == null) {throw new FileNotFoundException("Could not open " + getDescription());}return is;}public URL getURL() throws IOException {URL url = this.servletContext.getResource(this.path);if (url == null) {throw new FileNotFoundException(getDescription() + " cannot be resolved to URL because it does not exist");}return url;}public File getFile() throws IOException {URL url = this.servletContext.getResource(this.path);if (url != null && ResourceUtils.isFileURL(url)) {// Proceed with file system resolution...return super.getFile();}else {String realPath = WebUtils.getRealPath(this.servletContext, this.path);return new File(realPath);}}

其实就是封装了servletContext的getResourceAsStream、getResource、和getRealPath方法。

UrlResource:对java.net.url的封装,用来访任意可以用URL表示的对象,例如以file为前缀的路径、Http、Ftp等。

InputStreamResource:以输入流返回表示的资源,InputStreamResource被Spring开发者不推荐使用,只有在需要从输入流中多次读取的情况下才建议使用InputStreamResouce ,因为InputStreamResource的exists方法和isOpen方法都总是返回true,所以在没有发生异常的情况下调用者是没有办法判断该resource是否真正可用,这样就为程序带来了一定的风险。

/*** This implementation always returns {@code true}.*/@Overridepublic boolean exists() {return true;}/*** This implementation always returns {@code true}.*/@Overridepublic boolean isOpen() {return true;}

ByteArrayResource:二进制数组表示的资源,二进制数组可以在内存中通过程序构造,在程序没有实际文件,只有字节数据的情况下可以使用此方法进行上传数据。

以上就是Resource接口比较常用的一些实现类,Spring会根据容器上下文的类型自动返回对应的resource,但是有一个前提,资源表达式没有前缀,比如,FileSystemXmlApplicationContext,它默认返回的就是FileSystemResource,FileSystemXmlApplicationContext中的源码:

	protected Resource getResourceByPath(String path) {if (path.startsWith("/")) {path = path.substring(1);}return new FileSystemResource(path);}

如果容器上下文类型是ClassPathXmlApplicationContext,那么默认返回的就使ClasspathResource ,ClassPathXmlApplicationContext类中并没有显示定义返回ClassPathResource,而是定义在它的超类DefaultResourceLoader中, 源码:

	public Resource getResource(String location) {Assert.notNull(location, "Location must not be null");for (ProtocolResolver protocolResolver : this.protocolResolvers) {Resource resource = protocolResolver.resolve(location, this);if (resource != null) {return resource;}}if (location.startsWith("/")) {return getResourceByPath(location); //1}else if (location.startsWith(CLASSPATH_URL_PREFIX)) {return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());}else {try {// Try to parse the location as a URL...URL url = new URL(location);return new UrlResource(url);}catch (MalformedURLException ex) {// No URL -> resolve as resource path.return getResourceByPath(location);}}}

代码1处getResourceByPath返回的是ClassPathContextResource,ClassPathContextResource是ClassPathResource的子类,因此也可视为ClassPathResource。

protected Resource getResourceByPath(String path) {return new ClassPathContextResource(path, getClassLoader());}

如果想改变这种默认的行为,开发者可以使用带前缀的,比如:classpath:、file 等,spring就会根据前缀类型返回对应的resource。

总结:在日常程序开发中,处理外部资源是很繁琐的事情,我们可能需要处理URL资源、File资源资源、ClassPath相关资源、服务器相关资源等等很多资源。因此处理这些资源需要使用不同的接口,这就增加了我们系统的复杂性,如果能抽象出一个统一的接口来对这些底层资源进行统一访问,不仅方便了开发者使用,而且还增加了系统的整洁性。Spring Resource 提供了对底部资源的一致性访问接口,不管底层资源是URL资源、File资源资源、ClassPath相关资源或是别的spring统一返回Resource实例,这样一来,开发者就可以从操作底层资源类型的繁琐细节中脱离出来,更加专注于业务逻辑, 从而提升开发效率。

  相关解决方案