免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 1667 | 回复: 1

Spring 资源访问剖析和策略模式应用 [复制链接]

论坛徽章:
0
发表于 2012-03-19 16:53 |显示全部楼层
Spring 资源访问剖析和策略模式应用









Spring 框架是一个非常优秀的轻量级 Java 框架,它的资源访问方式高度简化了应用程序的资源访问,将应用程序从底层不同的资源访问中解放出来,消除了不同资源访问方式的差异。Spring 框架的资源访问就是经典设计模式:策略模式的典范应用,本文将从策略模式的角度来深入分析 Spring 资源访问的源码实现。


Spring 资源访问剖析和策略模式应用
Spring 把所有能记录信息的载体,如各种类型的文件、二进制流等都称为资源,对 Spring 开发者来说,最常用的资源就是 Spring 配置文件(通常是一份 XML 格式的文件)。
在 Sun 所提供的标准 API 里,资源访问通常由 java.net.URL 和文件 IO 来完成,尤其是当我们需要访问来自网络的资源时,通常会选择 URL 类。
URL 类可以处理一些常规的资源访问问题,但依然不能很好地满足所有底层资源访问的需要,比如,暂时还无法从类加载路径、或相对于 ServletContext 的路径来访问资源,虽然 Java 允许使用特定的 URL 前缀注册新的处理类(例如已有的 http: 前缀的处理类),但是这样做通常比较复杂,而且 URL 接口还缺少一些有用的功能,比如检查所指向的资源是否存在等。
Spring 改进了 Java 资源访问的策略。Spring 为资源访问提供了一个 Resource 接口,该接口提供了更强的资源访问能力,Spring 框架本身大量使用了 Resource 接口来访问底层资源。
Resource 接口是具体资源访问策略的抽象,也是所有资源访问类所实现的接口。Resource 接口主要提供了如下几个方法:
getInputStream():定位并打开资源,返回资源对应的输入流。每次调用都返回新的输入流。调用者必须负责关闭输入流。
exists():返回 Resource 所指向的资源是否存在。
isOpen():返回资源文件是否打开,如果资源文件不能多次读取,每次读取结束应该显式关闭,以防止资源泄漏。
getDescription():返回资源的描述信息,通常用于资源处理出错时输出该信息,通常是全限定文件名或实际 URL。
getFile:返回资源对应的 File 对象。
getURL:返回资源对应的 URL 对象。
Resource 和策略模式 Resource 接口就是策略模式的典型应用,Resource 接口就代表资源访问策略,但具体采用哪种策略实现,Resource 接口并不理会。客户端程序只和 Resource 接口耦合,并不知道底层采用何种资源访问策略,这样应用可以在不同的资源访问策略之间自由切换。
最后两个方法通常无须使用,仅在通过简单方式访问无法实现时,Resource 提供传统的资源访问的功能。
Resource 接口本身没有提供访问任何底层资源的实现逻辑,针对不同的底层资源,Spring 将会提供不同的 Resource 实现类,不同的实现类负责不同的资源访问逻辑。
Resource 不仅可在 Spring 的项目中使用,也可直接作为资源访问的工具类使用。意思是说:即使不使用 Spring 框架,也可以使用 Resource 作为工具类,用来代替 URL。当然,使用 Resource 接口会让代码与 Spring 的接口耦合在一起,但这种耦合只是部分工具集的耦合,不会造成太大的代码污染。
回页首
Resource 的实现类
Resource 接口是 Spring 资源访问策略的抽象,它本身并不提供任何资源访问实现,具体的资源访问由该接口的实现类完成——每个实现类代表一种资源访问策略。
Spring 为 Resource 接口提供了如下实现类:
UrlResource:访问网络资源的实现类。
ClassPathResource:访问类加载路径里资源的实现类。
FileSystemResource:访问文件系统里资源的实现类。
ServletContextResource:访问相对于 ServletContext 路径里的资源的实现类:
InputStreamResource:访问输入流资源的实现类。
ByteArrayResource:访问字节数组资源的实现类。
这些 Resource 实现类,针对不同的的底层资源,提供了相应的资源访问逻辑,并提供便捷的包装,以利于客户端程序的资源访问。
使用 UrlResource 访问网络资源
访问网络资源通过 UrlResource 类实现,UrlResource 是 java.net.URL 类的包装,主要用于访问之前通过 URL 类访问的资源对象。URL 资源通常应该提供标准的协议前缀。例如:file: 用于访问文件系统;http: 用于通过 HTTP 协议访问资源;ftp: 用于通过 FTP 协议访问资源等。
UrlResource 类实现 Resource 接口,对 Resource 全部方法提供了实现,完全支持 Resource 的全部 API。下面代码示范了使用 UrlResource 访问文件系统资源的示例。程序如下:

清单 1. UrlResourceTest.java
  1. public class UrlResourceTest
  2. {
  3. public static void main(String[] args) throws Exception
  4. {
  5. // 创建一个 Resource 对象,指定从文件系统里读取资源
  6. UrlResource ur = new UrlResource("file:book.xml");
  7. // 获取该资源的简单信息
  8. System.out.println(ur.getFilename());
  9. System.out.println(ur.getDescription());
  10. // 创建 Dom4j 的解析器
  11. SAXReader reader = new SAXReader();
  12. Document doc = reader.read(ur.getFile());
  13. // 获取根元素
  14. Element el = doc.getRootElement();
  15. List l = el.elements();
  16. // 此处省略了访问、输出 XML 文档内容的代码。
  17. ...
  18. }
  19. }
复制代码
上面程序中粗体字代码使用 UrlResource 来访问本地磁盘资源,虽然 UrlResource 是为访问网络资源而设计的,但通过使用 file 前缀也可访问本地磁盘资源。如果需要访问网络资源,可以使用如下两个常用前缀:
http:-该前缀用于访问基于 HTTP 协议的网络资源。
ftp:-该前缀用于访问基于 FTP 协议的网络资源。
由于 UrlResource 是对 java.net.URL 的封装,所以 UrlResource 支持的前缀与 URL 类所支持的前缀完全相同。
将应用所需的 book.xml 访问放在应用的当前路径,运行该程序,即可看到使用 UrlResource 访问本地磁盘资源的效果。
使用 ClassPathResource 访问类加载路径下的资源。
ClassPathResource 用来访问类加载路径下的资源,相对于其他的 Resource 实现类,其主要优势是方便访问类加载路径里的资源,尤其对于 Web 应用,ClassPathResource 可自动搜索位于 WEB-INF/classes 下的资源文件,无须使用绝对路径访问。
下面示例程序示范了将 book.xml 放在类加载路径下,然后使用如下程序访问它:

清单 2. ClassPathResourceTest.java

  1. public class ClassPathResourceTest
  2. {
  3. public static void main(String[] args) throws Exception
  4. {
  5. // 创建一个 Resource 对象,从类加载路径里读取资源
  6. ClassPathResource cr = new ClassPathResource("book.xml");
  7. // 获取该资源的简单信息
  8. System.out.println(cr.getFilename());
  9. System.out.println(cr.getDescription());
  10. // 创建 Dom4j 的解析器
  11. SAXReader reader = new SAXReader();
  12. Document doc = reader.read(cr.getFile());
  13. // 获取根元素
  14. Element el = doc.getRootElement();
  15. List l = el.elements();
  16. // 此处省略了访问、输出 XML 文档内容的代码。
  17. ...
  18. }
  19. }
复制代码
上面程序的粗体字代码用于访问类加载路径下的 book.xml 文件,对比前面进行资源访问的 2 个示例程序,我们发现两个程序除了进行资源访问的代码有所区别之外,其他程序代码基本一致,这就是 Spring 资源访问的优势:Spring 的资源访问消除了底层资源访问的差异,允许程序以一致的方式来访问不同的底层资源。
ClassPathResource 实例可使用 ClassPathResource 构造器显式地创建,但更多的时候它都是隐式创建的,当执行 Spring 的某个方法时,该方法接受一个代表资源路径的字符串参数,当 Spring 识别该字符串参数中包含 classpath: 前缀后,系统将会自动创建 ClassPathResource 对象。
使用 FileSystemResource 访问文件系统资源
Spring 提供的 FileSystemResource 类用于访问文件系统资源,使用 FileSystemResource 来访问文件系统资源并没有太大的优势,因为 Java 提供的 File 类也可用于访问文件系统资源。
当然使用 FileSystemResource 也可消除底层资源访问的差异,程序通过统一的 Resource API 来进行资源访问。下面程序是使用 FileSystemResource 来访问文件系统资源的示例程序。

清单 3. FileSystemResourceTest.java
  1. public class FileSystemResourceTest
  2. {
  3. public static void main(String[] args) throws Exception
  4. {
  5. // 默认从文件系统的当前路径加载 book.xml 资源
  6. FileSystemResource fr = new FileSystemResource("book.xml");
  7. // 获取该资源的简单信息
  8. System.out.println(fr.getFilename());
  9. System.out.println(fr.getDescription());
  10. // 创建 Dom4j 的解析器
  11. SAXReader reader = new SAXReader();
  12. Document doc = reader.read(fr.getFile());
  13. // 获取根元素
  14. Element el = doc.getRootElement();
  15. List l = el.elements();
  16. // 此处省略了访问、输出 XML 文档内容的代码。
  17. ...
  18. }
  19. }
复制代码
与前两种 Resource 作资源访问的区别在于:资源字符串确定的资源,位于本地文件系统内 ,而且无须使用任何前缀。
FileSystemResource 实例可使用 FileSystemResource 构造器显式地创建。但更多的时候它都是隐式创建的,执行 Spring 的某个方法时,该方法接受一个代表资源路径的字符串参数,当 Spring 识别该字符串参数中包含 file: 前缀后,系统将会自动创建 FileSystemResource 对象。
通过上面代码不难发现,程序使用 UrlResource、FileSystemResource、ClassPathResource 三个实现类来访问资源的代码差异并不大,唯一的缺点在于客户端代码需要与 Resource 接口的实现类耦合,这依然无法实现高层次解耦。
这对于策略模式来说将没有任何问题,策略模式要解决的就是这个问题,策略模式将会提供一个 Context 类来为客户端代码“智能”地选择策略实现类。至此我们发现了 Spring 资源访问的两个重要部分:Resource 接口和多个实现类,它们之间有如图 1 所示

图 1.Spring 资源访问的策略接口和策略实现类

图 1 所示的类图中提供了一个 Resouce 接口,这个接口就是 Spring 为资源访问所提供的策略接口,该接口下的大量实现类:UrlResource、ClassPathResource、FileSystemResource、ServletContextResource、ByteArrayResource、InputStreamReource 都实现了该策略接口,用于实现不同的资源访问策略。
下面我们将通过一个浅显的示例来讲解策略模式:
回页首
策略模式
策略模式用于封装系列的算法,这些算法通常被封装在一个被称为 Context 类中,客户端程序可以自由选择其中一种算法,或让 Context 为客户端选择一个最佳的算法——使用策略模式的优势是为了支持算法的自由切换。
考虑如下场景:现在我们正在开发一个网上书店,该书店为了更好地促销,经常需要对图书进行打折促销,程序需要考虑各种打折促销的计算方法。
为了实现书店现在所提供的各种打折需求,程序考虑使用如下方式来实现
// 一段实现 discount() 方法代码
  1. public double discount(double price)
  2. {
  3. // 针对不同情况采用不同的打折算法
  4. switch(getDiscountType())
  5. {
  6. case VIP_DISCOUNT:
  7.     return vipDiscount(price);
  8. case OLD_DISCOUNT:
  9.     return oldDiscount(price);
  10. case SALE_DISCOUNT:
  11.     return saleDiscount(price);
  12. ...
  13. }
  14. }
复制代码
上面粗体字代码会根据打折类型来决定使用不同的打折算法,从而满足该书店促销打折的要求。从功能实现的角度来看,这段代码没有太大的问题。但这段代码有一个明显的不足,程序中各种打折方法都被直接写入了 discount(double price) 方法中。如有一天,该书店需要新增一种打折类型呢?那开发人员必须修改至少三处代码:首先需要增加一个常量,该常量代表新增的打折类型;其次需要在 switch 语句中增加一个 case 语句;最后开发人员需要实现 xxxDiscount() 方法,用于实现新增的打折算法。
为了改变这种不好的设计,下面将会选择使用策略模式来实现该功能,下面先提供一个打折算法的接口,该接口里包含一个 getDiscount () 方法,该接口代码如下:

清单 4. DiscountStrategy.java

  1. public interface DiscountStrategy
  2. {
  3. //定义一个用于计算打折价的方法
  4. double getDiscount(double originPrice);
  5. }
复制代码
对于程序中这个 DiscountStrategy 接口而言,实现该接口的实现类就可以实现打折,但具体的打折策略与该接口无关,而是由具体的实现类来决定打折策略。由此可见,这个 DiscountStrategy 接口的作用和 Spring 框架中 Resource 接口的作用完全相同。
就像 Spring 框架必须为 Resource 接口提供大量实现类一样,我们此处也需要为 DiscountStrategy 接口提供两个实现类,每个实现类代表一种打折策略。
下面代表 VIP 打折策略

清单 5. VipDiscount.java
  1. // 实现 DiscountStrategy 接口,实现对 VIP 打折的算法
  2. public class VipDiscount
  3. implements DiscountStrategy
  4. {
  5. // 重写 getDiscount() 方法,提供 VIP 打折算法
  6. public double getDiscount(double originPrice)
  7. {
  8. System.out.println("使用 VIP 折扣 ...");
  9. return originPrice * 0.5;
  10. }
  11. }
复制代码
下面代表旧书打折策略

清单 6. OldDiscount.java
  1. public class OldDiscount
  2. implements DiscountStrategy
  3. {
  4. // 重写 getDiscount() 方法,提供旧书打折算法
  5. public double getDiscount(double originPrice)
  6. {
  7. System.out.println("使用旧书折扣 ...");
  8. return originPrice * 0.7;
  9. }
  10. }
复制代码
此时遇到了与前面程序相同的问题,如果客户端代码直接与具体的策略类(如 VIPDiscount、OldDiscount)耦合,那客户端代码将无法实现解耦。因此策略模式需要为客户端代码提供了一个 Context 类,让它为客户端代码决定采用哪种策略。例如本示例程序提供一个 DiscountContext 类,该类用于为客户端代码选择合适折扣策略,当然也允许用户自由选择折扣策略。下面是该 DiscountContext 类的代码:

清单 7. DiscountContext.java
  1. public class DiscountContext
  2. {
  3. // 组合一个 DiscountStrategy 对象
  4. private DiscountStrategy strategy;
  5. // 构造器,传入一个 DiscountStrategy 对象
  6. public DiscountContext(DiscountStrategy strategy)
  7. {
  8. this.strategy  = strategy;
  9. }
  10. // 根据实际所使用的 DiscountStrategy 对象得到折扣价
  11. public double getDiscountPrice(double price)
  12. {
  13. // 如果 strategy 为 null,系统自动选择 OldDiscount 类
  14. if (strategy == null)
  15. {
  16. strategy = new OldDiscount();
  17. }
  18. return this.strategy.getDiscount(price);
  19. }
  20. // 提供切换算法的方法
  21. public void changeDiscount(DiscountStrategy strategy)
  22. {
  23. this.strategy = strategy;
  24. }
  25. }
复制代码
从上面程序的粗体字代码可以看出,该 Context 类扮演了决策者的角色,它决定调用哪个折扣策略来处理图书打折。当客户端代码没有选择合适的折扣时,该 Context 会自动选择 OldDiscount 折扣策略;用户也可根据需要选择合适的折扣策略。
下面程序示范了客户端代码使用该 Contex 类来处理图书打折:

论坛徽章:
0
发表于 2012-03-19 16:54 |显示全部楼层
谢谢分享
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP