8.4.4 Spring的AOP支持

Spring中的AOP代理由SpringloC容器负责生成、管理,其依赖关系也由IoC容器负责管理。因此,AOP代理可以直接使用容器中的其他Bean实例作为目标,这种关系可由IoC容器的依赖注入提供。

Spring创建AOP代理的方式

Spring默认使用Java动态代理来创建AOP代理,这样就可以为任何接口实例创建代理了。
Spring也可以使用cglib代理,在需要代理类而不是代理接口的时候, Spring会自动切换为使用cglib代理。但Spring推荐使用面向接口编程,因此业务对象通常都会实现一个或多个接口,此时默认将使用JDK动态代理,但也可强制使用cglib代理。

Spring AOP使用纯Java实现。它不需要特定的编译工具, Spring AOP也不需要控制类装载器层次,因此它可以在所有的Java Web容器或应用服务器中运行良好。

Spring目前仅支持将方法调用作为连接点

Spring目前仅支持将方法调用作为连接点( Joinpoint),如果需要把对成员变量的访问和更新也作为增强处理的连接点,则可以考虑使用AspectJ

Spring侧重于AOP实现和Spring IoC容器的支持

Spring实现AOP的方法跟其他的框架不同。 Spring并不是要提供最完整的AOP实现(尽管SpringAOP有这个能力), Spring侧重于AOP实现和Spring IoC容器之间的整合,用于帮助解决企业级开发中的常见问题。
因此, SpringAOP通常和Spring loC容器一起使用, Spring AOP从来没有打算通过提供一种全面的AOP解决方案来与AspectJ竟争。 Spring AOP采用基于代理的AOP实现方案,而AspectJ则采用编译时增强的解决方案。
Spring可以无缝地整合Spring AOPIoCAspectJ.,使得所有的AOP应用完全融入基于Spring的框架中,这样的集成不会影响Spring AOP API或者AOP Alliance API, Spring AOP保持了向下兼容性,依然允许直接使用Spring AOP API来完成AOP编程。

AOP编程中需要程序员参与的部分

一旦掌握了上面AOP的相关概念,不难发现进行AOP编程其实是很简单的事情。纵观AOP编程,其中需要程序员参与的只有三个部分。

  1. 定义普通业务组件
  2. 定义切入点,一个切入点可能横切多个业务组件。
  3. 定义增强处理,增强处理就是在AOP框架为普通业务组件织入的处理动作。

其中第一个部分是最平常不过的事情,所以无须额外说明。那么进行AOP编程的关键就是定义切入点和定义增强处理。一旦定义了合适的切入点和增强处理,AOP框架将会自动生成AOP代理,而AOP代理的方法大致有如下公式:
AOP代理的方法=增强处理+目标对象的方法

Spring定义切入点和增强处理的方式

通常建议使用AspectJ方式来定义切入点和增强处理,在这种方式下, Spring依然有如下两种选择来定义切入点增强处理

  1. 基于注解的”零配置”方式:使用@Aspect@Pointcut等注解来标注切入点和增强处理。
  2. 基于XML配置文件的管理方式:使用Spring配置文件来定义切入点和增强处理

8.4.3 AOP的基本概念

AOP从程序运行角度考虑程序的流程,提取业务处理过程的切面。AOP面向的是程序运行中各个步骤,希望以更好的方式来组合业务处理的各个步骤。

AOP框架特性

AOP框架并不与特定的代码耦合,AOP框架能处理程序执行中特定的切入点( Pointcut),而不与某个具体类耦合。AOP框架具有如下两个特征

  1. 各步骤之间的良好隔离性。
  2. 源代码无关性。

面向切面编程的一些术语

下面是关于面向切面编程的一些术语。

术语 描述
切面Aspect 切面用于组织多个Advice, Advice放在切面中定义。
连接点Joinpoint 程序执行过程中明确的点,如方法的调用,或者异常的抛出。在Spring AOP中,连接点总是方法的调用。
增强处理Advice AOP框架在特定的切入点执行的增强处理。处理有"around""before""after"等类型
术语 描述
切入点Pointcut 可以插入增强处理的连接点。简而言之,当某个连接点满足指定要求时,该连接点将被添加增强处理,该连接点也就变成了切入点。
例如如下代码:
1
2
pointcut xxxPointcut()
:execution(void H*.say*())

每个方法被调用都只是连接点,但如果该方法属于H开头的类,且方法名以say开头,则该方法的执行将变成切入点。如何使用表达式来定义切入点是AOP的核心, Spring默认使用Aspect切入点语法

术语 描述
引入 将方法或字段添加到被处理的类中。 Spring允许将新的接口引入到任何被处理的对象中。例如,你可以使用一个引入,使任何对象实现IsModified接口,以此来简化缓存。
目标对象 AOP框架进行增强处理的对象,也被称为被增强的对象。如果AOP框架采用的是动态AOP实现,那么该对象就是一个被代理的对象.
AOP代理 AOP框架创建的对象,简单地说,代理就是对目标对象的加强Spring中的AOP代理可以是JDK动态代理,也可以是cglib代理。
织入Weaving 将增强处理添加到目标对象中,并创建一个被增强的对象的过程就是织入。织入有两种实现方式—编译时增强(如AspectJ)和运行时增强(如Spring AOP)。Spring和其他纯Java AOP框架一样,在运行时完成织入

由前面的介绍知道,AOP代理就是由AOP框架动态生成的一个对象,该对象可作为目标对象使用AOP代理包含了目标对象的全部方法,但AOP代理中的方法与目标对象的方法存在差异:AOP方法在特定切入点添加了增强处理,并回调了目标对象的方法。
AOP代理所包含的方法与目标对象的方法示意图如图8.9所示。
这里有一张图片

8.4.2 使用AspectJ实现AOP

AspectJ是一个基于Java语言的AOP框架,提供了强大的AOP功能,其他很多AOP框架都借鉴或采纳其中的一些思想。由于SpringAOPAspectJ进行了很好的集成,因此掌握AspectJ是学习Spring AOP的基础。
AspectJJava语言的一个AOP实现,其主要包括两个部分:

  • 一个部分定义了如何表达、定义AOP编程中的语法规范,通过这套语法规范,可以方便地用AOP来解决Java语言中存在的交叉关注点的问题;
  • 另一个部分是工具部分,包括编译器、调试工具等。

AspectJ是最早的、功能比较强大的AOP实现之一,对整套AOP机制都有较好的实现,很多其他语言的AOP实现,也借鉴或采纳了AspectJ中的很多设计。在Java领域, AspectJ中的很多语法结构基本上已成为AOP领域的标准。
Spring2.0开始, Spring AOP已经引入了对AspectJ的支持,并允许直接使用AspectJ进行AOP编程,而Spring自身的AOP API也努力与Aspect保持一致。因此,学习Spring AOP就必然需要从AspectJ开始,因为它是Jawa领域最流行的AOP解决方案。即使不用Spring框架,也可以直接使用AspectJ进行AOP编程。
AspectJEclipse下面的一个开源子项目,其最新的1.9.0RC2版本(1.9系列才支持Java9)于2017年11月9日发布,这也是本书所使用的Aspect版本。

1. 下载和安装AspectJ

下载和安装AspectJ请按如下步骤进行。

如何下载AspectJ

  1. 登录AspectJ站点,下载Aspect的最新版本1.9.x,本书下载AspectJ1.9.0.RC2版本。
  2. 下载完成后得到一个aspectj-1.9.0.RC2.jar文件,该文件名中的1.9.0表示AspectJ的版本号。

如何安装AspectJ

  1. 启动命令行窗口,进入aspectj-19.0.RC2.jar文件所在的路径,输入命令: java -jar aspectj-19.0.RC2.jar,将看到如图8.5所示的对话框。
    这里有一张图片
  2. 单击"Next"按钮,系统将出现如图8.6所示的对话框,该对话框用于选择JDK安装路径.、
    这里有一张图片
  3. 如果JDK安装路径正确,则直接单击"Next"按钮;否则应该通过右边的"Browse"按钮来选择JDK安装路径。正确选择了JDK安装路径后单击"Next"按钮,系统将出现如图8.7所示的对话框,该对话框用于选择AspectJ的安装路径。
    这里有一张图片
  4. 选择了合适的安装路径后,单击"Install"按钮,程序开始安装AspectJ,安装结束后出现一个对话框,单击该对话框中的"Next"按钮,将弹出安装完成对话框,如图8.8所示。
  5. 正如图8.8中所提示的,安装了AspectJ之后,系统还应该将AspectJ安装目录下的bin路径添加到PATH环境变量中,将AspectJ安装目录下的lib目录下的aspectjrt.jar添加到CLASSPATH环境变量中。
    1. Aspect提供了编译、运行Aspect的一些工具命令,这些工具命令放在AspectJbin路径下,而lib路径下的aspectjrt.jar则是Aspect.的运行时环境,所以需要分别添加这两个环境变量——就像安装了JDK也需要添加环境变量一样.

AspectJ是纯绿色软件

本书没有将Aspect安装在C盘,这是因为AspectJ是”纯绿色”软件,安装Aspect的实质是解压缩了一个压缩包,并不需要向Windows注册表、系统路径里添加任何”垃圾”信息,因此保留Aspect安装后的文件夹,即使以后重装Windows系统, AspectJ也不会受到任何影响。

2. AspectJ使用入门

成功安装了AspectJ之后,将会在AspectJ的安装路径中看到如下文件结构。

  1. bin:该路径下存放了ajaj5ajcajdocajbrowser等命令,其中ajc命令最常用,它的作用类似于Javac,用于对普通的Java类进行编译时增强
  2. docs:该路径下存放了AspectJ的使用说明、参考手册、API文档等文档。
  3. lib:该路径下的4个JAR文件是AspectJ的核心类库。
  4. 相关授权文件。

虽然AspectJEclipse基金组织的开源项目,而且提供了EclipseADT插件( Aspect Development Tools)来开发AspectJ应用,但AspectJ并不依赖于Eclipse工具。
实际上, AspectJ的用法非常简单,就像使用JDK编译、运行Java程序一样。

程序示例

1
2
3
4
5
6
7
G:\Desktop\随书源码\轻量级Java EE企业应用实战(第5版)\codes\08\8.4\AspectJQs
├─AspectJTest.java
├─AuthAspect.java
├─Hello.java
├─LogAspect.java
├─TxAspect.java
└─World.java

下面通过一个简单的程序来示范AspectJ的用法。

Hello.java

首先编写两个简单的Java类,这两个Java类用于模拟系统中的业务组件,实际上无论多少个类,AspectJ的处理方式都是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.crazyit.app.service;

public class Hello
{
// 定义一个简单方法,模拟应用中的删除用户的方法
public void deleteUser(Integer id)
{
System.out.println("执行Hello组件的deleteUser删除用户:" + id);
}
// 定义一个addUser()方法,模拟应用中的添加用户的方法
public int addUser(String name , String pass)
{
System.out.println("执行Hello组件的addUser添加用户:" + name);
return 20;
}
}

World.java

另一个World组件类如下。

1
2
3
4
5
6
7
8
9
10
package org.crazyit.app.service;

public class World
{
// 定义一个简单方法,模拟应用中的业务逻辑方法
public void bar()
{
System.out.println("执行World组件的bar()方法");
}
}

上面两个业务组件类总共定义了三个方法,用于模拟系统所包含的三个业务逻辑方法,实际上无论多少个方法, AspectJ的处理方式都是一样的。

AspectJTest.java

下面使用一个主程序来模拟系统调用两个业务组件的三个业务方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package lee;

import org.crazyit.app.service.Hello;
import org.crazyit.app.service.World;

public class AspectJTest
{
public static void main(String[] args)
{
Hello hello = new Hello();
hello.addUser("孙悟空" , "7788");
hello.deleteUser(1);
World world = new World();
world.bar();
}
}

使用最原始的javac.exe命令来编译这三个源程序:

1
2
3
G:\Desktop\随书源码\轻量级Java EE企业应用实战(第5版)\codes\08\8.4\AspectJQs>javac -d . Hello.java
G:\Desktop\随书源码\轻量级Java EE企业应用实战(第5版)\codes\08\8.4\AspectJQs>javac -d . World.java
G:\Desktop\随书源码\轻量级Java EE企业应用实战(第5版)\codes\08\8.4\AspectJQs>javac -d . AspectJTest.java

然后使用java.exe命令来执行AspectJTest类,程序输出如下:

1
2
3
4
G:\Desktop\随书源码\轻量级Java EE企业应用实战(第5版)\codes\08\8.4\AspectJQs>java lee.AspectJTest
执行Hello组件的addUser添加用户:孙悟空
执行Hello组件的deleteUser删除用户:1
执行World组件的bar()方法

假设现在客户要求在执行所有业务方法之前先执行权限检査,如果使用传统的编程方式,开发者必须先定义一个权限检査的方法,然后由此打开每个业务方法,并修改业务方法的源代码,增加调用权限检查的方法—但这种方式需要对所有业务组件中的每个业务方法都进行修改,因此不仅容易引入新的错误,而且维护成本相当大。

AuthAspect.java

如果使用AspectJAOP支持,则只要添加如下特殊的"Java类"即可:

1
2
3
4
5
6
7
8
9
10
11
12
package org.crazyit.app.aspect;

public aspect AuthAspect
{
// 指定在执行org.crazyit.app.service包中任意类的、任意方法 之前 执行下面代码块
// 第一个星号表示返回值不限;第二个星号表示类名不限;
// 第三个星号表示方法名不限;圆括号中..代表任意个数、类型不限的形参
before(): execution(* org.crazyit.app.service.*.*(..))
{
System.out.println("模拟进行权限检查...");
}
}

可能读者已经发现了,上面的类文件中不是使用classinterfaceenum定义Java类,而是使用了aspect难道Java又新增了关键字?没有!上面的AuthAspect根本不是一个Java类,所以aspect也不是Java支持的关键字,它只是AspectJ才能识别的关键字。
上面的代码也不是方法,它只是指定在执行某些类的某些方法之前, AspectJ将会自动先调用该代码块中的代码。
正如前面提到的,Java无法识别AuthAspect.java文件的内容,所以要使用ajc.bat命令来编译上面的Java程序.

1
ajc -1.8 -d . *.java

可以把ajc.bat理解成增强版的javac.exe命令,都用于编译Java程序,区别是ajc.bat命令可识别AspectJ的语法。

由于ajc命令默认兼容JDK1.4源代码,因此它默认不支持自动装箱、自动拆箱等功能。所以上面使用该命令时指定了-1.8选项,表明让ajc命令兼容JDK1.8
运行该AspectJTest类依然无须任何改变,还是使用如下命令运行AspectJTest类:

1
java lee.AspectJTest

运行该程序,将看到一个令人惊喜的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
G:\Desktop\Test\AspectJQs>mytree f
G:\Desktop\Test\AspectJQs
├─AspectJTest.java
├─AuthAspect.java
├─Hello.java
└─World.java

G:\Desktop\Test\AspectJQs>ajc -1.8 -d . *.java

G:\Desktop\Test\AspectJQs>java lee.AspectJTest
模拟进行权限检查...
执行Hello组件的addUser添加用户:孙悟空
模拟进行权限检查...
执行Hello组件的deleteUser删除用户:1
模拟进行权限检查...
执行World组件的bar()方法

从上面的运行结果来看,完全不需要对Hello.javaWorld.java等业务组件进行任何修改,但同时又可以满足客户的需求—上面的程序只是在控制台打印了"模拟进行权限检査"这个字符串来模拟权限检查,实际上也可用实际的权限检查代码来代替这行简单的语句,这就可以满足客户需求了。

LogAspect.java

如果客户再次提出新需求,比如需要在执行所有的业务方法之后增加记录日志的功能,那也很简单,只要再定义一个LogAspect,程序如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.crazyit.app.aspect;

public aspect LogAspect
{
// 定义一个PointCut,其名为logPointcut,
// 该Pointcut代表了后面给出的切入点表达式,这样可复用该切入点表达式
pointcut logPointcut()
:execution(* org.crazyit.app.service.*.*(..));
after():logPointcut()
{
System.out.println("模拟记录日志...");
}
}

上面程序中的粗体字代码定义了一个pointcut logPointcut(),这种用法就是为后面的切入点表达式起个名字,方便后面复用这个切入点表达式—假如程序中有多个代码块需要使用该切入点表达式,这些代码块都可直接复用此处定义的logPointcut,,而不是重复书写烦琐的切入点表达式。
再次使用如下命令来编译上面的Java程序:

1
ajc -1.8 -d . *.java

再次运行lee.AspectJTest类,将看到如下运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
G:\Desktop\Test\AspectJQs>ajc -1.8 -d . *.java

G:\Desktop\Test\AspectJQs>java lee.AspectJTest
模拟进行权限检查...
执行Hello组件的addUser添加用户:孙悟空
模拟记录日志...
模拟进行权限检查...
执行Hello组件的deleteUser删除用户:1
模拟记录日志...
模拟进行权限检查...
执行World组件的bar()方法
模拟记录日志...

TxAspect.java

假如现在需要在业务组件的所有业务方法之前启动事务,并在方法执行结束时关闭事务,同样只要定义如下TXAspect即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.crazyit.app.aspect;

public aspect TxAspect
{
// 指定在执行org.crazyit.app.service包中任意类的、任意方法时执行下面代码块
Object around():call(* org.crazyit.app.service.*.*(..))
{
System.out.println("== 模拟开启事务...");
// 回调原来的目标方法
Object rvt = proceed();
System.out.println("== 模拟结束事务...");
return rvt;
}
}

上面的代码:Object rvt = proceed();指定proceed代表回调原来的目标方法,这样位于proceed代码之前的代码就会被添加在目标方法之前,位于proceed代码之后的代码就会被添加在目标方法之后。

如果再次使用ajc.bat命令来编译上面所有的Java类,并执行lee.AspectJTest,此时将会发现系统中两个业务组件所包含的业务方法已经变得”十分强大”了,但并未修改过Hello.javaWorld.java的源代码

AspectJ的作用

这就是**AspectJ的作用:开发者无须修改源代码,但又可以为这些组件的方法添加新的功能。**

AspectJ在编译时增强类的功能

如果读者安装过Java的反编译工具,则可以反编译前面程序生成的Hello.classWorld.class文件,将发现该Hello.classWorld.class文件不是由Hello.javaWorld.java文件编译得到的,Hello.class,World.class里新增了很多内容,这表明AspectJ在编译时已增强了Hello.class, World.class类的功能,因此AspectJ通常被称为编译时增强的AOP框架。

AOP达到的效果

AOP要达到的效果是,保证在程序员不修改源代码的前提下,为系统中业务组件的多个业务方法添加某种通用功能。但AOP的实际上依然要去修改业务组件的多个业务方法的源代码,只不过是这个修改由AOP框架完成的,不需要程序员手动修改。

AOP实现分类

AOP实现按AOP框架修改源代码的时机可分为两类:

  1. 静态AOP实现:AOP框架在编译阶段对程序进行修改,即实现对目标类的增强,生成静态的AOP代理类(生成的*.class文件已经被改掉了,需要使用特定的编译器)。这以AspectJ为代表。
  2. 动态AOP实现:AOP框架在运行阶段动态生成AOP代理(在内存中以JDK动态代理或cglib动态地生成AOP代理类),以实现对目标对象的增强。这以Spring AOP为代表。

静态AOP性能好需要编译器

一般来说,静态AOP实现具有较好的性能,但需要使用特殊的编译器。动态AOP实现是纯Java实现,因此无须特殊的编译器,但是通常性能略差。

Spring AOP

Spring AOP就是动态AOP实现的代表, Spring AOP不需要在编译时对目标类进行增强,而是在运行时生成目标类的代理类,该代理类要么与目标类实现相同的接口,要么作为目标类的子类,总之,代理类都对目标类进行了增强处理

  • 实现相同的接口方式是JDK动态代理的处理策略,
  • 作为目标类的子类cglib代理的处理策略。

一般来说,编译时增强的AOP框架在性能上更有优势,因为运行时动态增强的AOP框架需要每次运行时都进行动态增强。

可能有读者对AspectJ更深入的知识感兴趣,但本书的重点并不是介绍AspectJ,因此如果读者希望掌握如何定义AspectJ中的AspectPointcut等内容,可参考AspectJ安装路径下的doc目录里的quick5.pdf文件。

8.4 Spring的AOP

AOP(Aspect Orient Programming),也就是面向切面编程,作为面向对象编程(OOP)的一种补充,已经成为一种比较成熟的编程方式。AOPOOP互为补充,面向对象编程将程序分解成各个层次的对象,而面向切面编程将程序运行过程分解成各个切面。可以这样理解:

  • 面向对象编程是从静态角度考虑程序结构,
  • 面向切面编程则是从动态角度考虑程序运行过程.

8.4.1 为什么需要AOP

在传统的OOP编程里以对象为核心,整个软件系统由一系列相互依赖的对象组成,而这些对象将被抽象成一个个类,并允许使用类继承来管理类与类之间一般到特殊的关系。随着软件规模的增大,应用的逐渐升级,慢慢出现了一些OOP很难解决的问题。
面向对象可以通过分析抽象出一系列具有一定属性与行为的类,并通过这些类之间的协作来形成个完整的软件功能。由于类可以继承,因此可以把具有相同功能或相同特性的属性抽象到一个层次分明的类结构体系中。随着软件规范的不断扩大,专业化分工越来越细致,以及OOP应用实践的不断增多随之也暴露出了一些OOP无法很好解决的问题.
现在假设系统中有三段完全相同的代码,这些代码通常会采用”复制”、”粘贴”的方式来完成,通过这种”复制”、”粘贴”的方式开发出来的软件示意图如图8.3所示。
看到如图8.3所示的示意图,可能有的读者已经发现了这种做法的不足之处—如果有一天,图8.3中的深色代码段需要修改,那是不是要打开三个地方的代码进行修改?如果不是三个地方包含这段代码而是100个地方,甚至是1000个地方包含这个代码段,那会是什么后果?
为了解决这个问题,通常会将如图8.3所示的深色代码部分定义成一个方法,然后在三个代码段中分别调用该方法即可。在这种方式下,软件系统的结构示意图如图8.4所示。
这里有一张图片
对于如图8.4所示的软件系统,如果需要修改深色代码部分,只要修改一个地方即可。不管整个系统中有多少个地方调用了该方法,程序无须修改这些地方,只需修改被调用的方法即可—通过这种方式,大大降低了软件后期维护的复杂度。
对于如图8.4所示的方法1、方法2、方法3依然需要显式调用深色方法,这样做能够解决大部分应用场景。如果程序希望实现更好的解耦,希望方法1、方法2、方法3彻底与深色方法分离——方法1、方法2、方法3无须直接调用深色方法,那该如何解决?
因为软件系统需求变更是很频繁的事情,系统前期设计方法1、方法2、方法3时只实现了核心业务功能,过了一段时间,可能需要为方法1、方法2、方法3都增加事务控制;又过了一段时间,客户提出方法1、方法2、方法3需要进行用户合法性验证,只有合法的用户才能执行这些方法;又过了段时间,客户又提出方法1、方法2、方法3应该增加日志记录…面这样的情况,应该怎么处理呢?通常有两种做法。

  • 根据需求说明书,直接拒绝客户要求。
  • 拥抱需求,满足客户的需求

第一种做法显然不好,客户是上帝,开发者应该尽量满足客户的需求。通常会釆用第二种做法,那如何解决呢?是不是每次都先定义一个新方法,然后修改方法1、方法2、方法3的源代码,增加调用新方法?这样做的工作量也不小啊!此时就希望有一种特殊的方式:只要实现新的方法,然后无须在方法1方法2、方法3中显式调用它,系统会”自动”在方法1、方法2、方法3中调用这个特殊的新方法
上面的自动执行的”自动”被加上了引号,是因为在编程过程中,没有所谓自动的事情,任何事情都是代码驱动的。这里的自动是指无须开发者关心,由系统来驱动。

上面的想法听起来很神奇,甚至有一些不切实际,但其实是完全可以实现的,实现这个需求的技术就是AOPAOP专门用于处理系统中分布于各个模块(不同方法)中的交叉关注点的问题,Java EE应用中,常常通过AOP来处理一些具有横切性质的系统级服务,如事务管理、安全检查、缓存、对象池管理等,AOP已经成为一种非常常用的解决方案。

8.3.4 在ApplicationContext中使用资源

不管以怎样的方式创建ApplicationContext实例,都需要为ApplicationContext指定配置文件, Spring允许使用一份或多份XML配置文件。
当程序创建ApplicationContext实例时,通常也是以Resource的方式来访问配置文件的,所以ApplicationContext完全支持ClassPathResource, FileSystemResourceServletContextResource等资源访问方式。 ApplicationContext确定资源访问策略通常有两种方法。

  1. 使用ApplicationContext实现类指定访问策略。
  2. 使用前缀指定访问策略。

1. 使用ApplicationContext实现类指定访问策略

创建ApplicationContext对象时,通常可以使用如下三个实现类。

ApplicationContext实现类 对应的Resource实现类
ClassPathXmlApplicatinContext 对应使用ClassPathResource进行资源访问。
FileSystemXmlApplicationContext 对应使用FileSystemResoure进行资源访问
XmlWebApplicationContext 对应使用ServletContextResource进行资源访问。

从上面的说明可以看出,当使用ApplicationContext的不同实现类时,就意味着Spring使用相应的资源访问策略。
当使用如下代码来创建Spring容器时,则意味着从本地文件系统来加载XML配置文件。

1
2
//从本地文件系统的当前路径加载 beans.xml文件创建Spring容器
ApplicationContext ctx=new FileSystemXmlApplicationContext("beans.xml");

程序从本地文件系统的当前路径下读取beans.xml文件,然后加载该资源,并根据该配置文件来创建ApplicationContext实例。相应的,采用ClassPathApplicationContext实现类,则从类加载路径下加载XML配置文件.

PS:据我观察当前路径默认是项目的跟路径,类加载路径默认是src目录的路径

2. 使用前缀指定访问策略

Spring也允许使用前缀来指定资源访问策略,例如,采用如下代码来创建ApplicationContext:

1
2
ApplicationContext ctx=new
FileSystemXmlApplicationContext("classpath:beans.xml");

虽然上面的代码采用了FileSystemXmlApplicationContext实现类,但程序依然从类加载路径下搜索beans.xml配置文件,而不是从本地文件系统的当前路径下搜索。相应的,还可以使用http:,ftp:等前缀,用来确定对应的资源访问策略。
看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package lee;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.core.io.Resource;

public class SpringTest
{
public static void main(String[] args) throws Exception
{
@SuppressWarnings("resource")
ApplicationContext ctx = new FileSystemXmlApplicationContext(
"classpath:beans.xml");
System.out.println(ctx);
// 使用ApplicationContext的资源访问策略来访问资源,没有指定前缀
Resource r = ctx.getResource("book.xml");
System.out.println(r.getClass());
System.out.println(r.getDescription());
}
}

Resource实例的输出结果是:

1
2
3
org.springframework.context.support.FileSystemXmlApplicationContext@439f5b3d, started on Tue Sep 03 00:36:36 CST 2019
class org.springframework.core.io.FileSystemResource
file [E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\ApplicationContext\book.xml]

上面程序中创建Spring容器时,系统将从类加载路径下搜索beans.xml;但使用ApplicationContext来访问资源时,依然采用的是FileSystemResource实现类,这与FileSystemXmlApplicationContext的访问策略是一致的。

classpath:前缀指定资源访问策略仅仅对当次访问有效

这表明:通过classpath:前缀指定资源访问策略仅仅对当次访问有效,程序后面进行资源访问时,还是会根据AppliactionContext的实现类来选择对应的资源访问策略.

ApplicationContext访问资源时建议显示采用对应实现类来加载配置文件

因此,如果程序需要使用ApplicationContext访问资源,建议显式采用对应的实现类来加载配置文件,而不是通过前缀来指定资源访问策略。当然,也可在每次进行资源访问时都指定前缀,让程序根据前缀来选择资源访问策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package lee;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.core.io.Resource;

public class SpringTest
{
public static void main(String[] args) throws Exception
{
@SuppressWarnings("resource")
ApplicationContext ctx = new FileSystemXmlApplicationContext(
"classpath*:beans.xml");
System.out.println(ctx);
// 使用ApplicationContext的资源访问策略来访问资源,通过 classpath:前缀指定策略
Resource r = ctx.getResource("classpath:book.xml");
System.out.println(r.getClass());
System.out.println(r.getDescription());
}
}

运行结果:

1
2
3
org.springframework.context.support.FileSystemXmlApplicationContext@439f5b3d, started on Tue Sep 03 00:40:59 CST 2019
class org.springframework.core.io.ClassPathResource
class path resource [book.xml]

由此可见,如果每次进行资源访问时都指定了前缀,则系统会采用前缀相应的资源访问策略

3. classpath*:前缀的用法

classpath*:前缀提供了加载多个XML配置文件的能力,当使用classpath*:前缀来指定XML配置文件时,系统将搜索类加载路径,找出所有与文件名匹配的文件,分别加载文件中的配置定义,最后合并成一个ApplicationContext。看如下代码:

将配置文件beans.xml分别放在应用的classes路径(该路径被设为类加载路径之一)下,并将配置文件放在classes/aa路径下(该路径也被设为类加载路径之一),程序实例化ApplicationContext时显示:

从上面的执行结果可以看出,当使用classpath*:前缀时, Spring将会搜索类加载路径下所有满足该规则的配置文件。
如果不是采用classpath*:前缀,而是改为使用classpath:前缀, Spring则只加载第一个符合条件的XML文件。例如如下代码:

1
2
ApplicationContext ctx=
new FileSystemXmlApplicationContext("classpath:beans.xml");

8.3.3 使用Resource作为属性

前面介绍了Spring提供的资源访问策略,但这些依赖访问策略要么使用Resource实现类,要么使用ApplicationContext来获取资源。实际上,当应用程序中的Bean实例需要访问资源时, Spring有更好的解决方法:直接利用依赖注入.
归纳起来,如果Bean实例需要访问资源,则有如下两种解决方案

  1. 在代码中获取Resource实例。
  2. 使用依赖注入

对于第一种方式的资源访问,当程序获取Resource实例时,总需要提供Resource所在的位置,不管通过FileSystemResource创建实例,还是通过ClassPathResource创建实例,或者通过ApplicationContextgetResource()方法获取实例,都需要提供资源位置。这意味着:资源所在的物理位置将被耦合到代码中,如果资源位置发生改变,则必须改写程序。因此,通常建议采用第二种方法,让SpringBean实例依赖注入资源。

程序示例

1
2
3
4
5
6
7
8
9
10
11
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\Inject_Resource
└─src\
├─beans.xml
├─book.xml
├─lee\
│ └─SpringTest.java
└─org\
└─crazyit\
└─app\
└─service\
└─TestBean.java

看如下TestBean,它有一个Resource类型的res实例变量,程序为该实例变量提供了对应的setter方法,这就可以利用Spring的依赖注入了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package org.crazyit.app.service;

import java.util.Iterator;
import java.util.List;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.springframework.core.io.Resource;

public class TestBean
{
private Resource resource;

// res的setter方法
public void setResource(Resource resource)
{
this.resource = resource;
}
public void parse() throws Exception
{
// 获取该资源的简单信息
System.out.println(resource.getFilename());
System.out.println(resource.getDescription());
// 创建基于SAX的dom4j的解析器
SAXReader reader = new SAXReader();
Document doc = reader.read(resource.getFile());
// 获取根元素
Element el = doc.getRootElement();
List l = el.elements();
// 遍历根元素的全部子元素
for (Iterator it = l.iterator(); it.hasNext();)
{
// 每个节点都是<书>节点
Element book = (Element) it.next();
List ll = book.elements();
// 遍历<书>节点的全部子节点
for (Iterator it2 = ll.iterator(); it2.hasNext();)
{
Element eee = (Element) it2.next();
System.out.println(eee.getText());
}
}
}
}

上面程序中定义了一个Resource类型的res属性,该属性需要接受Spring的依赖注入。除此之外,程序中的parse方法用于解析res资源所代表的XML文件。

beans.xml

在容器中配置该Bean,并为该Bean指定资源文件的位置。配置文件如下。

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="GBK"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="test" class="org.crazyit.app.service.TestBean"
p:resource="classpath:book.xml" />
</beans>

上面配置文件中的p:res="classpath:book.xml"属性配置了资源的位置,并使用了classpath:前缀,这指明让Spring从类加载路径下加载book.xml文件。
与前面类似的是,此处的前缀也可采用http:ftp:file:等,这些前缀将强制Spring采用对应的资源访问策略(也就是指定具体使用哪个Resource实现类);
如果不采用任何前缀,则Spring将采用与该ApplicationContext相同的资源访问策略来访问资源。

使用依赖注入资源的优点

采用依赖注入,允许动态配置资源文件位置,无须将资源文件位置写在代码中,当资源文件位置发生变化时,无须改写程序,直接修改配置文件即可。

8.3.2 ResourceLoader接口和 ResourceLoaderAware接口

Spring提供如下两个标志性接口。

接口 描述
Resourceloader 该接口实现类的实例可以获得一个Resource实例。
ResourceLoaderAware 该接口实现类的实例将获得一个Resourceloader的引用.

ResourceLoader接口

ResourceLoader接口方法

ResourceLoader接口里只有一个方法,如下所示:

方法 描述
Resource getResource(String location) 该方法用于返回一个Resource实例。

ApplicationContext的实现类都实现ResourceLoader接口,因此ApplicationContext可用于直接获取Resource实例。

某个ApplicationContext实例获取Resource实例时,默认采用与ApplicationContext相同的资源访问策略。看如下代码:

1
2
3
ApplicationContext ctx = new 实现类(
"beans.xml");
Resource resource = ctx.getResource("book.xml");

从上面的代码中无法确定Spring用哪个实现类来访问指定资源,

ApplicationContext使用相同策略访问资源是什么意思

Spring将采用和ApplicationContext相同的策略来访问资源。就是说:

  • 如果这个ApplicationContextFileSystemXmlApplicationContext,则getResource()方法获取到的resource就是FileSystemResource实例;
  • 如果这个ApplicationContextClassPathXmlApplicationContext,则getResource()方法获取到的resource就是ClassPathResource实例;
  • 如果这个ApplicationContextXmlWebApplicationContext,则getResource()方法获取到的resource就是ServletContextResource实例。

从上面的介绍可以看出,当Spring应用需要进行资源访间时,实际上并不需要直接使用Resource实现类,而是调用ResourceLoader实例的getResource()方法来获得资源。 ResourceLoader将会负责选择Resource的实现类,也就是确定具体的资源访问策略,从而将应用程序和具体的资源访问策略分离开来,这就是典型的策略模式。

程序示例

1
2
3
4
5
6
7
8
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\ResourceLoader
├─beans.xml
├─book.xml
└─src\
├─beans.xml
├─book.xml
└─lee\
└─ResourceLoaderTest.java

看如下示例程序,将使用ApplicationContext来访问资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package lee;
import org.springframework.context.*;
import org.springframework.context.support.*;
import org.springframework.core.io.Resource;
import org.dom4j.*;
import org.dom4j.io.*;
import java.util.*;

public class ResourceLoaderTest
{
public static void main(String[] args) throws Exception
{
// 创建ApplicationContext实例
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"beans.xml");
// ApplicationContext ctx = new
// FileSystemXmlApplicationContext("beans.xml");
Resource res = ctx.getResource("book.xml");

// 获取该资源的简单信息
System.out.println(res.getFilename());
System.out.println(res.getDescription());
// 创建基于SAX的dom4j解析器
SAXReader reader = new SAXReader();
Document doc = reader.read(res.getFile());
// 获取根元素
Element el = doc.getRootElement();
List l = el.elements();
// 遍历根元素的全部子元素
for (Iterator it = l.iterator(); it.hasNext();)
{
// 每个节点都是<书>节点
Element book = (Element) it.next();
List ll = book.elements();
// 遍历<书>节点的全部子节点
for (Iterator it2 = ll.iterator(); it2.hasNext();)
{
Element eee = (Element) it2.next();
System.out.println(eee.getText());
}
}
}
}

上面程序中的第一行代码创建了一个ApplictionContext对象,第二行代码通过该ApplictionContext对象来获取资源。虽然上面程序并未明确指定采用哪一种Resource实现类,而是仅仅通过ApplicactionContext获得Resource。不过由于使用的是ClassPathApplicationContext来获取资源,所以Spring将会使用ClassPathResource实现类从类加载路径下访问资源。

程序执行结果如下:

1
2
3
4
5
6
7
book.xml
class path resource [book.xml]
疯狂Java讲义
李刚
轻量级Java EE企业应用实战
李刚

从运行结果可以看出, 通过ClassPathXmlApplicationContext获取到的ResourceClassPathResource实现类。
如果将ApplicationContext改为使用FileSystemXmlApplicationContext 实现类,运行上面程序,将看到如下运行结果:

1
2
3
4
5
6
book.xml
file [E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\ResourceLoader\book.xml]
疯狂Java讲义
李刚
疯狂iOS讲义
李刚

从上面的执行结果可以看出,程序的Resource实现类发生了改变,变为使用FileSystemResource实现类。

为了保证得到上面的两次运行结果,需要分别在类加载路径(src/目录)下、当前文件路径(项目路径)下放置beans.xml:和book.xml两个文件(为了区分,本示例故意让两个路径下的book.xml文件略有区别)

使用前缀强制指定使用哪个Resource实现类

另外,使用ApplicationContext访问资源时,也可不理会ApplicationContext的实现类,强制使用指定的ClassPathResourceFileSystemResource等实现类,这可通过不同前缀来指定,如下面的代码所示。

1
2
//通过 classpath:前缀,强制使用ClasspathResource
Resource r= ctx.getResource("classpath:beans.xml");

类似的,还可以使用标准的java.net.URL前缀来强制使用UrlResource,如下所示:

1
2
3
4
//通过标准的file:前缀,强制使用UrlResource访问本地文件资源
Resource res=ctx.getResource("file:beans.xml");
//通过标准的http:前缀,强制使用UrlResource访问基于HTTP协议的网络资源
Resource res=ctx.getResource("http://localhost:8888/beans.xml");

常见前缀及其对应的访问策略

以下是常见的前缀及对应的访问策略。

前缀 描述
classpath: ClassPathResource实例访问类加载路径下的资源。
file: UrlResource实例访问本地文件系统的资源
http: UrlResource实例访问基于HTTP协议的网络资源。
无前缀 ApplicationContext的具体实现类来决定访问策略。

ResourceLoaderAware

ResourceLoaderAware完全类似于Spring提供的BeanFactoryAwareBeanNameAware接口。
ResourceLoaderAware接口提供了一个setResourceLoader()方法,该方法将由Spring容器负责调用,Spring容器会传入一个ResourceLoader对象作为setResourceLoader()方法的参数。
如果把实现ResourceLoaderAware接口的Bean类部署在Spring容器中, Spring容器会将自身当成ResourceLoader作为参数传入setResourceLoader()方法。由于ApplicationContext的实现类都实现了ResourceLoader接口, Spring容器自身完全可作为ResourceLoader使用。

程序示例

1
2
3
4
5
6
7
8
9
10
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\ResourceLoaderAware
└─src\
├─beans.xml
├─lee\
│ └─SpringTest.java
└─org\
└─crazyit\
└─app\
└─service\
└─TestBean.java

例如,如下Bean类实现了ResourceAware接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.crazyit.app.service;

import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.ResourceLoader;

public class TestBean implements ResourceLoaderAware
{
private ResourceLoader rd;
// 实现ResourceLoaderAware接口必须实现的方法
// 如果把该Bean部署在Spring容器中,该方法将会由Spring容器负责调用
// Spring容器调用该方法时,Spring容器会将自身作为参数传给该方法
public void setResourceLoader(ResourceLoader resourceLoader)
{
System.out.println("--执行setResourceLoader 方法--");
this.rd = resourceLoader;
}
// 返回ResourceLoader对象的引用
public ResourceLoader getResourceLoader()
{
return rd;
}
}

将该类部署在Spring容器中, Spring将会在创建完该Bean的实例之后,自动调用该BeansetResourceLoader()方法,调用该方法时会将容器自身作为参数传入。

如果需要验证这一点,程序可用TestBeangetResourceLoader()方法的返回值与Spring容器进行"=="运算符进行比较,将会发现使用"=="比较返回true,这表明两个引用指向相同的对象.

主类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package lee;

import org.crazyit.app.service.TestBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.ResourceLoader;

public class SpringTest
{
public static void main(String[] args)
{
// 创建ApplicationContext容器
@SuppressWarnings("resource")
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"beans.xml");
// 获取容器中名为test的Bean实例
TestBean tb = ctx.getBean("test", TestBean.class);
// 通过tb实例来获取ResourceLoader对象
ResourceLoader rl = tb.getResourceLoader();
// 判断程序获得ResourceLoader和Spring容器是否是同一个对象
System.out.println(rl == ctx);
}
}

运行结果:

1
2
--执行setResourceLoader 方法--
true

8.3.1 Resource实现类

Resource接口是Spring资源访问的接口,具体的资源访问由该接口的实现类完成。 Spring提供了Resource接口的大量实现类。

Resource接口实现类 描述
UrlResource 访问网络资源的实现类。
ClassPathResource 访问类加载路径里资源的实现类。
FileSystemResource 访问文件系统里资源的实现类。
ServletContextResource 访问相对于ServletContext路径下的资源的实现类.
InputStreamResource 访问输入流资源的实现类。
ByteArrayResource 访问字节数组资源的实现类.

针对不同的底层资源,这些Resource实现类提供了相应的资源访问逻辑,并提供便捷的包装,以利于客户端程序的资源访问。

1. 访问网络资源

访问网络资源通过UrlResource类实现, UrlResourcejava.net.URL类的包装,主要用于访问之前通过URL类访问的资源对象。URL资源通常应该提供标准的协议前缀。例如:file:用于访问文件系统;http:于通过HTTP协议访问资源;ftp:用于通过FTP协议访问资源等。

UrlResource类实现了Resource接口,对Resource的全部方法提供了实现,完全支持Resource的全部API

程序示例

1
2
3
4
5
6
7
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\UrlResource
├─book.xml
├─lib\
│ └─dom4j-1.6.1.jar
└─src\
└─lee\
└─UrlResourceTest.java

下面的代码示范了使用UrlResource访问文件系统资源的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package lee;

import org.springframework.core.io.UrlResource;
import org.dom4j.*;
import org.dom4j.io.*;
import java.util.*;

public class UrlResourceTest
{
public static void main(String[] args) throws Exception
{
// 创建一个Resource对象,指定从文件系统里读取资源
UrlResource ur = new UrlResource("file:book.xml");

// 获取该资源的简单信息
System.out.println(ur.getFilename());
System.out.println(ur.getDescription());
// 创建基于SAX的dom4j解析器
SAXReader reader = new SAXReader();
Document doc = reader.read(ur.getFile());
// 获取根元素
Element el = doc.getRootElement();
List l = el.elements();
// 遍历根元素的全部子元素
for (Iterator it = l.iterator(); it.hasNext();)
{
// 每个节点都是<书>节点
Element book = (Element) it.next();
List ll = book.elements();
// 遍历<书>节点的全部子节点
for (Iterator it2 = ll.iterator(); it2.hasNext();)
{
Element eee = (Element) it2.next();
System.out.println(eee.getText());
}
}
}
}

上面程序中的粗体字代码使用UrlResource来访问本地磁盘资源,虽然UrlResource是为访问网络资源而设计的,但通过使用file:前缀也可访问本地磁盘资源。如果需要访问网络资源,则可以使用如下两个常用前缀。

  1. http:,该前缀用于访问基于HTTP协议的网络资源。
  2. ftp:,该前缀用于访问基于FTP协议的网络资源。

由于UrIResource是对java.net.URL的封装,所以UrIResource支持的前缀与URL类所支持的前缀完全相同。
将应用所需的book.xml访问放在应用的当前路径下,运行该程序,即可看到使用UrlResource访问本地磁盘资源的效果。

1
2
3
4
5
6
book.xml
URL [file:book.xml]
疯狂Java讲义
李刚
轻量级Java EE企业应用实战
李刚

2. 访问类加载路径下的资源

ClassPathResource用来访问类加载路径下的资源,相对于其他的Resource实现类,其主要优势是方便访问类加载路径下的资源,尤其对于Web应用, ClassPathResource可自动搜索位于WEB-INF/classes下的资源文件,无须使用绝对路径访问.

程序示例

1
2
3
4
5
6
7
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\ClassPathResource
├─lib\
│ └─dom4j-1.6.1.jar
└─src\
├─book.xml
└─lee\
└─ClassPathResourceTest.java

下面示例程序示范了将book xml放在类加载路径(src目录)下,然后使用如下程序访问它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package lee;

import java.util.Iterator;
import java.util.List;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.springframework.core.io.ClassPathResource;

public class ClassPathResourceTest
{
public static void main(String[] args)
throws Exception
{
// 创建一个Resource对象,从类加载路径里读取资源
ClassPathResource cr = new ClassPathResource("book.xml");

// 获取该资源的简单信息
System.out.println(cr.getFilename());
System.out.println(cr.getDescription());
// 创建基于SAX的dom4j解析器
SAXReader reader = new SAXReader();
Document doc = reader.read(cr.getFile());
// 获取根元素
Element el = doc.getRootElement();
List l = el.elements();
// 遍历根元素的全部子元素
for (Iterator it = l.iterator();it.hasNext() ; )
{
// 每个节点都是<书>节点
Element book = (Element)it.next();
List ll = book.elements();
// 遍历<书>节点的全部子节点
for (Iterator it2 = ll.iterator();it2.hasNext() ; )
{
Element eee = (Element)it2.next();
System.out.println(eee.getText());
}
}
}
}

上面程序中的粗体字代码用于访问类加载路径下的book.xml文件,对比前面进行资源访问的示例程序,发现两个程序除了进行资源访问的代码有所区别之外,其他程序代码基本一致,这就是Spring资源访问的优势— Spring的资源访问消除了底层资源访问的差异,允许程序以一致的方式来访问不同的底层资源。

3. 访问文件系统资源

Spring提供的File SystemResource类用于访问文件系统资源。不过使用FileSystemResource来访问文件系统资源并没有太大的优势,因为Java提供的File类也可用于访问文件系统资源。
当然,使用FileSystemResource也可消除底层资源访问的差异,程序通过统一的Resource API来进行资源访问。

程序示例

1
2
3
4
5
6
7
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\FileSystemResource
├─book.xml
├─lib\
│ └─dom4j-1.6.1.jar
└─src\
└─lee\
└─FileSystemResourceTest.java

下面的程序是使用FileSystemResource来访问文件系统资源的示例程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package lee;

import java.util.Iterator;
import java.util.List;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.springframework.core.io.FileSystemResource;

public class FileSystemResourceTest
{
public static void main(String[] args) throws Exception
{
// 默认从文件系统的当前路径加载book.xml资源
FileSystemResource fr = new FileSystemResource("book.xml");

// 获取该资源的简单信息
System.out.println(fr.getFilename());
System.out.println(fr.getDescription());
// 创建基于SAX的dom4j解析器
SAXReader reader = new SAXReader();
Document doc = reader.read(fr.getFile());
// 获取根元素
Element el = doc.getRootElement();
List l = el.elements();
// 遍历根元素的全部子元素
for (Iterator it = l.iterator();it.hasNext() ; )
{
// 每个节点都是<书>节点
Element book = (Element)it.next();
List ll = book.elements();
// 遍历<书>节点的全部子节点
for (Iterator it2 = ll.iterator();it2.hasNext() ; )
{
Element eee = (Element)it2.next();
System.out.println(eee.getText());
}
}
}
}

与前两种使用Resource进行资源访问的区别在于:用于确定的资源的字符串写法不同,位于本地文件系统内,而且无须使用任何前缀。
FileSystemResource实例可使用FileSystemResource构造器显式地创建,但更多的时候它都是隐式创建的。执行Spring的某个方法时,该方法接受一个代表资源路径的字符串参数,当Spring识别该字符串参数中包含file:前缀后,系统将会自动创建FileSystemResource对象。

4. 访问应用相关资源

Spring提供了ServletContextResource类来访问Web Context下相对路径下的资源,ServletContextResource构造器接受一个代表资源位置的字符串参数,该资源位置是相对于Web应用根路径的位置
使用ServletContextResource访问的资源,也可通过文件IO访问或URL访问。

访问Web Context下的资源时使用File类ServletContextResource的区别

  • 通过java.io.File访问要求资源被解压缩,而且在本地文件系统中;
  • 但使用ServletContextResource进行访问时则无须关心资源是否被解压缩出来,或者直接存放在JAR文件中,总可通过Servlet容器访问。

当程序试图直接通过File来访问Web Context下相对路径下的资源时,应该先使用ServletContextgetRealPath()方法来取得资源绝对路径,再以该绝对路径来创建File对象。

程序示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\ServletContextResource
├─src\
└─WebContent\
├─META-INF\
│ └─MANIFEST.MF
├─test.jsp
└─WEB-INF\
├─book.xml
├─lib\
│ ├─dom4j-1.6.1.jar
│ ├─spring-aop-5.0.2.RELEASE.jar
│ ├─spring-aspects-5.0.2.RELEASE.jar
│ ├─spring-beans-5.0.2.RELEASE.jar
│ ├─spring-context-5.0.2.RELEASE.jar
│ ├─spring-context-indexer-5.0.2.RELEASE.jar
│ ├─spring-context-support-5.0.2.RELEASE.jar
│ ├─spring-core-5.0.2.RELEASE.jar
│ ├─spring-expression-5.0.2.RELEASE.jar
│ ├─spring-instrument-5.0.2.RELEASE.jar
│ ├─spring-jcl-5.0.2.RELEASE.jar
│ ├─spring-jdbc-5.0.2.RELEASE.jar
│ ├─spring-jms-5.0.2.RELEASE.jar
│ ├─spring-messaging-5.0.2.RELEASE.jar
│ ├─spring-orm-5.0.2.RELEASE.jar
│ ├─spring-oxm-5.0.2.RELEASE.jar
│ ├─spring-test-5.0.2.RELEASE.jar
│ ├─spring-tx-5.0.2.RELEASE.jar
│ ├─spring-web-5.0.2.RELEASE.jar
│ ├─spring-webflux-5.0.2.RELEASE.jar
│ ├─spring-webmvc-5.0.2.RELEASE.jar
│ └─spring-websocket-5.0.2.RELEASE.jar
└─web.xml

下面把book.xml文件放在web应用的WEB-INF路径下,然后通过JSP页面来直接访问该book.xm文件。值得指出的是,在默认情况下,JSP不能直接访问WEB-INF路径下的任何资源,所以该应用中的JSP页面需要使用ServletContextResource来访问该资源。
下面是JSP页面代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<%@ page contentType="text/html; charset=GBK" language="java"
errorPage=""%>
<%@ page
import="org.springframework.web.context.support.ServletContextResource"%>
<%@ page import="org.dom4j.*,org.dom4j.io.*,java.util.*"%>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>测试ServletContextResource</title>
</head>
<body>
<h3>测试ServletContextResource</h3>
<%
// 从Web Context下的WEB-INF路径下读取book.xml资源
ServletContextResource src = new ServletContextResource(application,
"WEB-INF/book.xml");
// 获取该资源的简单信息
System.out.println(src.getFilename());
System.out.println(src.getDescription());
// 创建基于SAX的dom4j解析器
SAXReader reader = new SAXReader();
Document doc = reader.read(src.getFile());
// 获取根元素
Element el = doc.getRootElement();
List l = el.elements();
// 遍历根元素的全部子元素
for (Iterator it = l.iterator(); it.hasNext();)
{
// 每个节点都是<书>节点
Element book = (Element) it.next();
List ll = book.elements();
// 遍历<书>节点的全部子节点
for (Iterator it2 = ll.iterator(); it2.hasNext();)
{
Element eee = (Element) it2.next();
out.println(eee.getText());
out.println("<br/>");
}
}
%>
</body>
</html>

上面程序中的粗体字代码指定应用从Web Context下的WEB-INF路径下读取book.xml资源,该示例恰好将book.xml文件放在应用的WEB-INF/路径下,通过使用ServletContextResource就可让JSP页面直接访问WEB-INF下的资源了。

将应用部署在Tomcat中,然后启动Tomcat,再打开浏览器访问该JSP页面,将看到浏览器显示内容如下:

1
2
3
4
5
6
测试ServletContextResource

疯狂Java讲义
李刚
轻量级Java EE企业应用实战
李刚

5. 访问字节数组资源

Spring提供了InputStreamResource来访问二进制输入流资源, InputSteamResource是针对输入流Resource实现,只有当没有合适的Resource实现时,才考虑使用该InputSteamResource。在通常情况下,优先考虑使用ByteArrayResource,或者基于文件的Resource实现.
与其他Resource实现不同的是, nputSteamResource是一个总是被打开的Resource,所以isOpen方法总是返回true。因此如果需要多次读取某个流,就不要使用InputSteamResource.
在创建InputStreamResource实例时要提供一个InputStream参数。
在一些个别的情况下, InputStreamResource是有用的。例如从数据库中读取一个Blob对象,程序需要获取该Blob对象的内容,就可先通过BlobgetBinaryStream()方法获取二进制输入流,再将该二进制输入流包装成Resource对象,然后就可通过该Resource对象来访问该Blob对象所包含的资源了。

尽量不要使用InputStreamResource

InputStreamResource虽然是适应性很广的Resource实现,但效率并不好。因此,尽量不要使用InputStreamResource作为参数,而应尽量使用ByteArrayResourceFileSystemResource代替它。

程序示例

1
2
3
4
5
6
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\ByteArrayResource
├─lib\
│ └─dom4j-1.6.1.jar
└─src\
└─lee\
└─ByteArrayResourceTest.java

如下程序示范了如何使用ByteArrayResource来读取字节数组资源。出于演示目的,程序中字节数组直接通过字符串来获得

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package lee;

import java.util.Iterator;
import java.util.List;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.springframework.core.io.ByteArrayResource;

public class ByteArrayResourceTest
{
public static void main(String[] args) throws Exception
{
// encoding设置为当前文件的编码,不然解析错误
String file = "<?xml version='1.0' encoding='UTF-8'?>"
+ "<计算机书籍列表><书><书名>疯狂Java讲义" + "</书名><作者>李刚</作者></书><书><书名>"
+ "轻量级Java EE企业应用实战</书名><作者>李刚" + "</作者></书></计算机书籍列表>";
byte[] fileBytes = file.getBytes();
// 以字节数组作为资源来创建Resource对象
ByteArrayResource bar = new ByteArrayResource(fileBytes);
// 获取该资源的简单信息
System.out.println(bar.getDescription());
// 创建基于SAX的dom4j解析器
SAXReader reader = new SAXReader();
Document doc = reader.read(bar.getInputStream());
// 获取根元素
Element el = doc.getRootElement();
List l = el.elements();
// 遍历根元素的全部子元素
for (Iterator it = l.iterator(); it.hasNext();)
{
// 每个节点都是<书>节点
Element book = (Element) it.next();
List ll = book.elements();
// 遍历<书>节点的全部子节点
for (Iterator it2 = ll.iterator(); it2.hasNext();)
{
Element eee = (Element) it2.next();
System.out.println(eee.getText());
}
}
}
}

上面程序中的粗体字代码用于根据字节数组来创建ByteArrayResource对象,接下来就可通过该Resource对象来访问该字节数组资源了。访问字节数组资源时, Resource对象的getFile()getFilename()两个方法不可用—这是可想而知的事情—因为此时访问的资源是字节数组,当然不存在对应的File对象和文件名了.
在实际应用中,字节数组可能通过网络传输获得,也可能通过管道流获得,还可能通过其他方式获得……只要得到了代表资源的字节数组,程序就可通过ByteArrayResource将字节数组包装成Resource实例,并利用Resource来访问该资源。
对于需要采用InputStreamResource访问的资源,可先从InputStream流中读出字节数组,然后以字节数组来创建ByteArrayResource。这样, InputStreamResource也可被转换成ByteArrayResource,从而方便多次读取。

8.3 资源访问

正如前面看到的,创建Spring容器时通常需要访问XML配置文件。除此之外,程序可能有大量地方需要访问各种类型的文件、二进制流等。

什么是资源

Spring把这些文件二进制流统称为资源

Java API提供的资源访问类

在官方提供的标准API里,资源访问通常由java.net.URL文件IO来完成,如果需要访问来自网络的资源,则通常会选择URL类。
URL类可以处理一些常规的资源访问问题,但依然不能很好地满足所有底层资源访问的需要,比如,暂时还无法在类加载路径或相对于 ServletContext的路径中访问资源。虽然Java允许使用特定的URL前缀注册新的处理类(例如已有的http:前缀的处理类),但是这样做通常比较复杂,而且URL接口还缺少一些有用的功能,比如检查所指向的资源是否存在等。

Spring提供Resource接口来访问资源

Spring改进了Java资源访问的策略, Spring为资源访问提供了一个Resource接口,该接口提供了更强的资源访问能力, Spring框架本身大量使用了Resource来访问底层资源。

Resource接口

Resource本身是一个接口,是具体资源访问策略的抽象,也是所有资源访问类要实现的接口
Resource接口主要提供如下几个方法。

方法 描述
getlnputStream() 定位并打开资源,返回资源对应的输入流。每次调用都返回新的输入流。调用者必须负责关闭输入流
exists() 判断Resource所指向的资源是否存在。
isOpen() 判断资源文件是否打开,如果资源文件不能多次读取,每次读取结束时应该显式关闭,以防止资源泄漏。
getDescription() 返回资源的描述信息,用于资源处理出错时输出该信息,通常是全限定文件名或实际URL
getFile() 返回资源对应的File对象。
getURL() 返回资源对应的URL对象。

最后的getFile()getURL()这两个方法通常无须使用,仅在通过简单方式访问无法实现时, Resource才提供传统的资源访问功能。

Resource接口本身没有提供访问任何底层资源的实现逻辑,针对不同的底层资源, Spring将会提供不同的Resource实现类,不同的实现类负责不同的资源访问逻辑。

SpringResource设计是一种典型的策略模式,通过使用Resource接口,客户端程序可以在不同的资源访问策略之间自由切换。关于策略模式请参考本书第9章。

Resource不仅可以在Spring的项目中使用,也可以直接作为资源访问的工具类使用。也就是说即使不使用Spring框架,也可以使用Resource来代替Java API中的URL类。当然,使用Resource接口会让代码与Spring的接口耦合在一起,但这种耦合只是部分工具集的耦合,不会造成太大的代码污染。

8.2.8 使用@Required检查注入

有些时候,可能会因为疏忽忘记为某个setter方法配置依赖注入:既没有显式通过<property.>配置依赖注入;也没有使用自动装配执行依赖注入。这种疏忽通常会导致由于被依赖组件没有被注入,当程序运行时调用被依赖组件的方法时就会引发NPE异常。
为了避免上面的疏忽,可以让Spring在创建容器时就执行检查,此时需要为setter方法添加@Required修饰,这时Spring会检查该setter方法:如果开发者既没有显式通过<property>元素配置依赖注入,也没有使用自动装配执行依赖注入, Spring容器会报BeanInitializationException异常。

程序示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\Required
└─src\
├─beans.xml
├─lee\
│ └─SpringTest.java
└─org\
└─crazyit\
└─app\
└─service\
├─Dog.java
├─impl\
│ ├─Chinese.java
│ └─GunDog.java
└─Person.java

例如,如下Chinese类使用@Required注解修饰了setGunDog()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Chinese implements Person
{
private Dog dog;
@Required
public void setGunDog(Dog dog)
{
this.dog = dog;
}
public void test()
{
System.out.println("我是一个普通人,养了一条狗:" + dog.run());
}
}

上面程序使用@Required修饰了setGunDog()方法,这意味着程序必须为该setter方法配置依赖注入:要么通过<property>元素配置设值注入,要么通过自动装配来执行依赖注入;否则Spring启动容器时就会引发异常。