首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

希望读到本书这儿时,你已认为Spring是个可靠的、支持充分的项目,具备成为应用开发之强大工具的所有要素。不过还缺一样——我们到现在还未展示任何代码。相信你一定渴望看到运转中的Spring,另外不来些代码我们也无法更进一步,心动不如行动吧。如果你不能完全理解这一节的所有代码,也别担心;随着阅读本书的深入,我们会逐步探究所有主题相关的更多细节。

当下我们暂且相信你完全熟悉传统的Hello World实例,除非过去这30年来你一直生活在月球上,代码2-1给出了其Java版本。

Listing 2-1: 典型的Hello World实例

package com.apress.prospring.ch2;

public class HelloWorld {

public static void main(String[] args) {

System.out.println("Hello World!");

}

}

就实例而言,这个算是相当简单——它确能工作但不太具有可扩展性。如果我们想要改变消息的内容呢,怎么办?如果我们想以不同方式输出这个消息呢,或许要输出到stderr而非stdout,或是要附上HTML标签而非只是普通文本?

我们打算重新定义示例应用程序的需求,比方说,它必须支持一个简单灵活的机制以方便改变消息的内容,它还必须能够简便的改变显示(rendering)状态。在简单的Hello World例子中,你可以轻而易举的做到上述两条,只需对代码做相应变动即可。然而,在一个更大的应用中,重新编译要耗费时间,还要求再次对应用进行完整的测试。显然这不可行,更好的解决方法是将消息的内容移到(代码)外部,并在运行时读入消息内容,比如从命令行参数,如代码2-2所示:

Listing 2-2: 使用命令行参数的Hello World

package com.apress.prospring.ch2;

public class HelloWorldWithCommandLine {

public static void main(String[] args) {

if(args.length > 0) {

System.out.println(args[0]);

} else {

System.out.println("Hello World!");

}

}

}

这个例子实现了我们的设想之一——现在无需改变代码就可以更改消息内容。不过该应用程序仍有不足:负责消息显示(render)的组件还是得负责消息的获取。要改变消息的获取方式也就等于要改变显示器(renderer)里的代码。姑且不论我们还无法轻易改变输出器这一事实;即使能做到这点,那就意味着要改变启动这个应用程序的那个类本身。

如果我们进一步修改这个应用程序,使之脱离简单基础的Hello World,那么更好的方法是重构显示(rendering)和消息获取逻辑,使之分别成为独立的组件。另外,如果真想让我们的应用程序变得灵活,我们还得让这些组件实现自接口(interface),并用这些接口确定组件和启动程序(launcher)之间的相关性。

为了重构消息获取逻辑,我们可以定义一个只有单个方法(method)getMessage()的简单接口MessageProvider,如代码2-3所示。

Listing 2-3: The MessageProvider Interface

package com.apress.prospring.ch2;

public interface MessageProvider {

public String getMessage();

}

而下面的代码2-4中,MessageRenderer接口会由可以显示(render)消息的所有组件实现。

Listing 2-4: The MessageRenderer Interface

package com.apress.prospring.ch2;

public interface MessageRenderer {

public void render();

public void setMessageProvider(MessageProvider provider);

public MessageProvider getMessageProvider();

}

如上所示,MessageRenderer接口有一个方法render(),还有一个JavaBean风格的属性(property)MessageProvider。任何MessageRenderer的实现都和消息获取部分相分离,而是把这部分功能委托(delegate)给提供了该功能的MessageProvider。由此MessageProvider从属于(依赖于??)MessageRender。创建这些接口的简单实现相当容易,可参看代码2-5。

Listing 2-5: The HelloWorldMessageProvider Class

package com.apress.prospring.ch2;

public class HelloWorldMessageProvider implements MessageProvider {

public String getMessage() {

return "Hello World!";

}

}

在代码2-5中,我们创建了一个简单的MessageProvider,返回的消息一成不变,总是”Hello World!”。

Listing 2-6: The StandardOutMessageRenderer Class

package com.apress.prospring.ch2;

public class StandardOutMessageRenderer implements MessageRenderer {

private MessageProvider messageProvider = null;

public void render() {

if (messageProvider == null) {

throw new RuntimeException(

"You must set the property messageProvider of class:"

+ StandardOutMessageRenderer.class.getName());

}

System.out.println(messageProvider.getMessage());

}

public void setMessageProvider(MessageProvider provider) {

this.messageProvider = provider;

}

public MessageProvider getMessageProvider() {

return this.messageProvider;

}

}

至此,剩下要做的就是重写入口类(entry class)的main()方法,如代码2-7所示。

Listing 2-7: Refactored Hello World

package com.apress.prospring.ch2;

public class HelloWorldDecoupled {

public static void main(String[] args) {

MessageRenderer mr = new StandardOutMessageRenderer();

MessageProvider mp = new HelloWorldMessageProvider();

mr.setMessageProvider(mp);

mr.render();

}

}

上述代码相当简单:我们先是具现(instantiate)了HelloWorldMessageProvider和 StandardOutMessageRenderer的实例,不过这两个实例声明时的型别(type)分别是MessageProvider和MessageRenderer。然后把MessageProvider传入MessageRenderer,最后调用MessageRenderer.render()。代码编译执行的输出如下,不出所料:

Hello World!

目前这个实例差不多就是我们苦苦寻找的,不过还是有个小问题。要想改变MessageRenderer或MessageProvider接口的实现(译注:如上述代码中的StandardOutMessageRenderer要换成YetAnotherMessageRenderer)就得改变代码。为了解决这个问题,我们可以创建一个简单的工厂类(factory class),负责从properties文件里读取实现类的名称,并为应用程序具现(instantiate)实现类,参看代码2-8。

Listing 2-8: The MessageSupportFactory Class

package com.apress.prospring.ch2;

import java.io.FileInputStream;

import java.util.Properties;

public class MessageSupportFactory {

private static MessageSupportFactory instance = null;

private Properties props = null;

private MessageRenderer renderer = null;

private MessageProvider provider = null;

private MessageSupportFactory() {

props = new Properties();

try {

props.load(new FileInputStream("ch2/src/conf/msf.properties"));

// get the implementation classes

String rendererClass = props.getProperty("renderer.class");

String providerClass = props.getProperty("provider.class");

renderer = (MessageRenderer)

Class.forName(rendererClass).newInstance();

provider = (MessageProvider)

Class.forName(providerClass).newInstance();

} catch (Exception ex) {

ex.printStackTrace();

}

}

static {

instance = new MessageSupportFactory();

}

public static MessageSupportFactory getInstance() {

return instance;

}

public MessageRenderer getMessageRenderer() {

return renderer;

}

public MessageProvider getMessageProvider() {

return provider;

}

}

上述实现琐碎而幼稚,错误处理也过于简单,此外配置文件的名称也是硬编码于代码中,不管怎样我们已经编写了一定量的代码。

更完整且适合于产品系统的实现显然需要更多代码,但就本章目的而言,示例代码已绰绰有余。这个类所用的配置文件相当简单,如下所示:

renderer.class=com.apress.prospring.ch2.StandardOutMessageRenderer

provider.class=com.apress.prospring.ch2.HelloWorldMessageProvider

对main()方法稍作修改(如代码2-9所示),我们即可达到目标:

Listing 2-9: Using MessageSupportFactory

package com.apress.prospring.ch2;

public class HelloWorldDecoupledWithFactory {

public static void main(String[] args) {

MessageRenderer mr =

MessageSupportFactory.getInstance().getMessageRenderer();

MessageProvider mp =

MessageSupportFactory.getInstance().getMessageProvider();

mr.setMessageProvider(mp);

mr.render();

}

}

在开始介绍如何把Spring引入到这个应用程序之前,先来温故知新一把。从最简单的Hello World应用程序开始,我们定义了该应用程序必须满足的两个要求。其一是消息内容应易于修改,其二是显示(render)机制也应易于修改。为满足上述要求,我们引入了两个接口:MessageProvider和MessageRenderer。为了能够获取要显示(render)的消息,MessageProvider接口依赖于 MessageRenderer接口的实现。最后,我们添加了一个简单的工厂类,由它获取实现类的名称并具现(instantiate)实现类以供使用。

用Spring进行重构

上一节的最后一个例子满足了我们为示例应用程序确定的目标,不过仍存在两个问题。其一,为了将整个应用程序拼在一起,我们必须编写大量胶水代码,尽管这样能够保证各组件之间的松散耦合。其二,我们还是必须人工为MessageRenderer实现提供一个MessageProvider实例(instance)。借助Spring,我们可以一并解决这两个问题。

为了解决胶水代码过多这一问题,我们完全可以移除应用程序里的MessageSupportFactory类,并用Spring的类DefaultListableBeanFactory取而代之。先不必深究这个类,眼下只要知道——这个类相当于较MessageSupportFactory更通用的版本,只不过玩了点小花样罢了,参看代码2-10。

Listing 2-10: Using DefaultListableBeanFactory

package com.apress.prospring.ch2;

import java.io.FileInputStream;

import java.util.Properties;

import org.springframework.beans.factory.BeanFactory;

import org.springframework.beans.factory.support.DefaultListableBeanFactory;

import org.springframework.beans.factory.support.PropertiesBeanDefinitionReader;

public class HelloWorldSpring {

public static void main(String[] args) throws Exception {

// get the bean factory

BeanFactory factory = getBeanFactory();

MessageRenderer mr = (MessageRenderer) factory.getBean("renderer");

MessageProvider mp = (MessageProvider) factory.getBean("provider");

mr.setMessageProvider(mp);

mr.render();

}

private static BeanFactory getBeanFactory() throws Exception {

// get the bean factory

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();

// create a definition reader

PropertiesBeanDefinitionReader rdr = new PropertiesBeanDefinitionReader(

factory);

// load the configuration options

Properties props = new Properties();

props.load(new FileInputStream("./ch2/src/conf/beans.properties"));

rdr.registerBeanDefinitions(props);

return factory;

}

}

如代码2-10所示,main()方法先是取得一个DefaultListableBeanFactory实例,其声明型别为BeanFactory,并由这个实例通过BeanFactory.getBean()方法取得MessageRenderer和MessageProvider实例。

眼下不必深究getBeanFactory()方法,只需知道这个方法负责从properties文件读取BeanFactory的配置信息,然后返回已配置的实例。这个properties文件和MessageSupportFactory所用的完全一致。

renderer.class=com.apress.prospring.ch2.StandardOutMessageRenderer

provider.class=com.apress.prospring.ch2.HelloWorldMessageProvider

BeanFactory接口和其实现类构成了Spring DI容器的核心。正如第1章所述,Spring在其DI容器中广泛使用了JavaBeans规范;鉴于此,Spring总是将受其管理的组件作为bean来引用,因此可以使用getBean()方法。

现在尽管我们在启动类里编写了更多代码,不过已无任何创建factory代码之需要,同时我们也获得了更为健壮的factory实现——错误处理更合理,还采用了完全分离的配置机制。不过,这段代码还是存有瑕疵。启动类必须知道MessageRenderer的依赖关系,并取得这些依赖关系,将其传给MessageRenderer。在代码2-10中,Spring不过是扮演了一个复杂工厂类的角色,负责创建和提供所需类的实例。最终,借助Spring的DI功能,我们就可以使用外部BeanFactory配置把整个应用程序粘合(glue)在一起。配置文件需要稍作修改。

# The MessageRenderer

renderer.class=com.apress.prospring.ch2.StandardOutMessageRenderer

renderer.messageProvider(ref)=provider

# The MessageProvider

provider.class=com.apress.prospring.ch2.HelloWorldMessageProvider

注意,除去注释,我们只添加了一行,这行负责把provider bean赋给renderer bean的MessageProvider(messageProvider?)属性。关键字(ref)表示这个属性的值作为引用(reference)而非文字值(literal value)传给另一个bean。使用这个配置,我们可以移除启动类对MessageProvider接口的所有引用,如代码2-11所示。

Listing 2-11: Using Dependency Injection

package com.apress.prospring.ch2;

import java.io.FileInputStream;

import java.util.Properties;

import org.springframework.beans.factory.BeanFactory;

import org.springframework.beans.factory.support.DefaultListableBeanFactory;

import org.springframework.beans.factory.support.PropertiesBeanDefinitionReader;

public class HelloWorldSpringWithDI {

public static void main(String[] args) throws Exception {

// get the bean factory

BeanFactory factory = getBeanFactory();

MessageRenderer mr = (MessageRenderer) factory.getBean("renderer");

mr.render();

}

private static BeanFactory getBeanFactory() throws Exception {

// get the bean factory

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();

// create a definition reader

PropertiesBeanDefinitionReader rdr = new PropertiesBeanDefinitionReader(

factory);

// load the configuration options

Properties props = new Properties();

props.load(new FileInputStream("./ch2/src/conf/beans.properties"));

rdr.registerBeanDefinitions(props);

return factory;

}

}

如上述代码所示,main()方法现在只要获取MessageRenderer bean,然后调用render();Spring已经创建了MessageProvider的实现并将其注入(inject)到MessageRenderer的实现中。注意对借助Spring组装在一起的类,我们无需再做任何改动。事实上,这些类和Spring任何部分都无关联,对Spring的存在也毫无觉察。不过也不必总是如此老死不相往来,你的类可以实现Spring专有接口,以便以多种方式和DI容器进行交互。例如,我们可以移除MessageRenderer.render()里的检查代码——查看是否已设置MessageProvider实现,并只进行一次检查。不过这要依赖Spring专有接口,与此相关的介绍留待第5章进行。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论