使用 System.out.println 打印一个消息和使用 Spring 打印消息的区别主要在于扩展性和耦合性的差异。Spring 如今已经成为了 Java 开发的公认标准了。

HelloWorld

虽然我相信每个接触过 Java 的开发者都知道以下代码的意思:

1
2
3
4
5
6
public class HelloWorld {

public static void main(String[] args) {
System.out.println("Hello World!");
}
}

这个例子非常简单,但不具备扩展性。如果想要改变消息、改变消息的输出方式(例如使用 HTML 标签而不是纯文本)都需要重新修改源码再进行编译。一个更好的做法是通过外部传入字符串参数:

1
2
3
4
5
6
7
8
9
10
public class HelloWorld {

public static void main(String[] args) {
if (args.length > 0) {
System.out.println(args[0]);
} else {
System.out.println("Hello World!");
}
}
}

现在可以不修改程序源码的情况下改变输出的消息。但是,这个程序负责渲染消息的组件(println )也同时负责获取消息。这就意味着改变渲染器就需要重新修改源码。

依赖注入:渲染器和消息分开

为了解除这种耦合关系,让这个程序变得更加灵活,应该让这些组件实现接口并定义组件和使用这些接口的程序之间的依赖关系。

重新编写消息提供的逻辑,采用一个接口来提供这种能力:

1
2
3
public interface MessageProvider {
String getMessage();
}

getMessage 本身就屏蔽了消息的获取机制,那么渲染器只要允许实现了这个接口的对象传入即可:

1
2
3
4
5
public interface MessageRenderer {
void render();
void setMessageProvider(MessageProvider provider);
MessageProvider getMessageProvider();
}

这样 MessageRenderer 就把提供消息的责任委托给了 MessageProvider

Provider 对应接口的实现也非常简单:

1
2
3
4
5
6
public class HelloWorldMessageProvider implements MessageProvider {
@Override
public String getMessage() {
return "Hello World!";
}
}

Renderer 则调用传入的对象的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class StandardOutMessageRenderer implements MessageRenderer{
private MessageProvider messageProvider;

@Override
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());
}

@Override
public void setMessageProvider(MessageProvider provider) {
this.messageProvider = provider;
}

@Override
public MessageProvider getMessageProvider() {
return this.messageProvider;
}
}

这种模式其实就是依赖注入的典型例子。入口的 main 方法则负责构建 Provider 和 Renderer,并将 Provider 作为依赖传入到 Renderer。

1
2
3
4
5
6
7
8
9
10
public class HelloWorld {

public static void main(String[] args) {
MessageRenderer messageRenderer = new StandardOutMessageRenderer();
MessageProvider messageProvider = new HelloWorldMessageProvider();
messageRenderer.setMessageProvider(messageProvider);
messageRenderer.render();
}
}

基于反射的工厂类

这个例子仍然有个小问题,如果修改 MessageRenderer 或者 MessageProvider 接口的实现,同样需要修改源码。为了解决这个问题,可以引入一个简单的工厂类,它从属性文件读取类的名称,并代表程序进行实例化:

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
public class MessageSupportFactory {
private static MessageSupportFactory instance;

private Properties props;
private MessageRenderer renderer;
private MessageProvider provider;

private MessageSupportFactory() {
props = new Properties();

try {
props.load(this.getClass().getResourceAsStream("./msf.properties"));
String renderClass = props.getProperty("renderer.class");
String providerClass = props.getProperty("provider.class");

renderer = (MessageRenderer) Class.forName(renderClass).getDeclaredConstructor().newInstance();
provider = (MessageProvider) Class.forName(providerClass).getDeclaredConstructor().newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}

static {
instance = new MessageSupportFactory();
}

public static MessageSupportFactory getInstance() {
return instance;
}

public MessageRenderer getMessageRenderer() {
return renderer;
}

public MessageProvider getMessageProvider() {
return provider;
}
}

创建对应的配置文件 msf.properties:

Text
1
2
renderer.class=com.ft.StandardOutMessageRenderer
provider.class=com.ft.HelloWorldMessageProvider

重新修改 main 进行初始化:

1
2
3
4
5
6
7
8
9
public class HelloWorld {

public static void main(String[] args) {
MessageRenderer mr = MessageSupportFactory.getInstance().getMessageRenderer();
MessageProvider mp = MessageSupportFactory.getInstance().getMessageProvider();
mr.setMessageProvider(mp);
mr.render();
}
}

至此,我们从 HelloWorld 开始,然而一步步让程序更加灵活。现在修改消息和渲染机制都很简单。

使用 Spring 重构

如果说上面的程序还有什么问题,那就是必须编写大量的胶水代码来将应用程序拼凑在一起。第二个问题就是仍然需要手动提供接口的对应实现类。 Spring 就可以用来解决这个问题。

可以将 MessageSupportFactory 完全删除,使用 Spring 的 ApplicationContext 替换它。这个接口存储 Spring 所管理的有关程序的所有的环境信息,并扩展了另外一个接口 ListableBeanFactory ,该接口充当 Spring 管理的任何 bean 实例应用程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.demo;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class DemoApplication {

public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring/app-context.xml");
MessageRenderer mr = ctx.getBean("renderer", MessageRenderer.class);
mr.render();
}
}

在这段代码中,可以看到 ClassPathXmlApplicationContextspring/app-context.xml 中获取应用程序的配置信息,类型为 ApplicationContext ,并通过使用 getBean 来获取 MessageRenderer 实例。

app-context.xml 的作用和之前的工厂类 MessageSupportFactory 是一样的:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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="provider" class="com.example.demo.HelloWorldMessageProvider"/>
<bean id="renderer" class="com.example.demo.StandardOutMessageRenderer" p:messageProvider-ref="provider"/>
</beans>

名称空间 <beans> 声明需要由 Spring 管理的 bean,并且声明他们之间的依赖需求。为了将 MessageProvider 注入到渲染器,可以使用属性 p

使用注解

从 Spring 3.0 开始,开发 Spring 不再需要 XML 配置文件,可以替换成注解和配置类。配置类是用 @Configuration 注解的 Java 类,它包含了 bean 定义(用 @bean 注解的方法),或者通过 @ComponentScanning 对 bean 定义进行注解,从而识别应用程序中 bean 的定义。

以下的配置类和前面的 bean-context.xml 等价:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class HelloWorldConfiguration {
// 等价于 <bean id="provider" class= ...
@Bean
public MessageProvider provider() {
return new HelloWorldMessageProvider();
}
// 等价于 <bean id="renderer" class= ...
@Bean
public MessageRenderer renderer() {
MessageRenderer renderer = new StandardOutMessageRenderer();
renderer.setMessageProvider(provider());
return renderer;
}
}

需要将 ClassPathXmlApplicationContext 方法替换成从配置类读取 bean 定义的 ApplicationContext 实现类 AnnotationConfigApplicationContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.demo;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class DemoApplication {

public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(HelloWorldConfiguration.class);
MessageRenderer mr = ctx.getBean("renderer", MessageRenderer.class);
mr.render();
}

}

小结

本文唯一做的一件事情就是打印 HelloWorld。从最原始的 HelloWorld,到程序启动参数控制,接着引入接口进行解耦。为了进一步让整个程序变得更加灵活,引入配置文件和工厂设计模式。Spring 则在这个基础上减少了大量胶水代码,让整个程序集中在业务层面,而这就是 Spring 最初的意义。当然,这篇只是碰到了 DI 的皮毛,并没有深入到 Spring 中的 IoC 和 DI。