希望读到本书这儿时,你已认为Spring是个可靠的、支持充分的项目,具备成为应用开发之强大工具的所有要素。不过还缺一样——我们到现在还未展示任何代码。相信你一定渴望看到运转中的Spring,另外不来些代码我们也无法更进一步,心动不如行动吧。如果你不能完全理解这一节的所有代码,也别担心;随着阅读本书的深入,我们会逐步探究所有主题相关的更多细节。
当下我们暂且相信你完全熟悉传统的Hello World实例,除非过去这30年来你一直生活在月球上,代码2-1给出了其Java版本。
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)实现类以供使用。
上一节的最后一个例子满足了我们为示例应用程序确定的目标,不过仍存在两个问题。其一,为了将整个应用程序拼在一起,我们必须编写大量胶水代码,尽管这样能够保证各组件之间的松散耦合。其二,我们还是必须人工为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章进行。





