下面看一段简单的测试代码:
代码片段8:
public static void main(String[] args)
{
ApplicationContext context =
New ClassPathXmlApplicationContext("applicationContext.xml");
Greeting greeting = (Greeting)context.getBean("Greeting");
greeting.greet();
}
这段代码很简单,如果你上文都看懂了,那么这里应该没有问题。值得注意的是Spring有两种方式来创建容器(我们不再用上文我们自己编写的Container),一种是ApplicationContext,另外一种是BeanFactory。ApplicationContext更强大一些,而且使用上两者没有太大区别,所以一般说来都用ApplicationContext。Spring容器帮助我们维护我们在配置文件中声明的Bean以及它们之间的依赖关系,我们的Bean只需要关注自己的核心业务。
四、面向接口的编程
看了这么多,也许你并没有觉得Spring给开发带来了很多便利。那是因为我举的例子还不能突出Spring的优越之处,接下来我将通过接口编程来体现Spring的强大。
假如现在要求扩展Greeting的功能,要让Speaker用不同的语言来问候,也就是说有不同的Speaker,比如ChineseSpeaker, EnglishSpeaker。那么对上文提到的三种编码方式(代码片段2、3、4)分别加以修改,你会发现很麻烦。假如下次又要加入一个西班牙语,又得重复劳动。很自然的会考虑到使用一个ISpeaker接口来简化工作,,更改后的代码如下(这里没有列出接口的相关代码,我想你应该明白怎么写):
代码片段9:
public void greet()
{
ISpeaker s = new ChineseSpeaker();
s.sayHello();
}
代码片段10:
public void greet()
{
ISpeaker s = (ISpeaker)context.lookup("ejb/ChineseSpeaker");
s.sayHello();
}
代码片段11:
public class Greeting
{
public ISpeaker s;
public Greet(ISpeaker s)
{
this.s = s;
}
public void greet()
{
s.sayHello();
}
}
对比三段代码,你会发现,第一种方法还是把具体的Speaker硬编码到代码中了,第二中方法稍微好一点,但是没有本质改变,而第三种方法就不一样了,代码中并没有关于具体Speaker的信息。也就是说,如果下次还有什么改动的话,第三种方法的Greeting类是不需要修改,编译的。根据上文Spring的使用介绍,只需要改动xml文件就能给Greeting注入不同的Speaker了,这样代码的扩展性是不是提高了很多?
关于Spring的接口编程还有很多东西可以去挖掘,后文还会提到有关Spring Proxy的接口编程,我这里先介绍这么多,有兴趣话可以去google更多的资料。
五、应用Spring中的切面
Spring生来支持AOP,首先来看几个概念:
1、切面(Aspect):切面是系统中抽象出来的的某一个功能模块,上文已经有过介绍,这里不再多说。
2、通知(Advice):通知是切面的具体实现。也就是说你的切面要完成什么功能,具体怎么做就是在通知里面完成的。这个名称似乎有点让人费解,等后面看了代码就明白了。
3、切入点(Pointcut):切入点定义了通知应该应用到系统的哪些地方。Spring只能控制到方法(有的AOP框架可以控制到属性),也就是说你能在方法调用之前或者之后选择切入,执行额外的操作。
4、目标对象(Target):目标对象是被通知的对象。它可以是任何类,包括你自己编写的或者第三方类。有了AOP以后,目标对象就只需要关注自己的核心业务,其他的功能,比如日志,就由AOP框架支持完成。
5、代理(Proxy):简单的讲,代理就是将通知应用到目标对象后产生的对象。Spring在运行时会给每个目标对象生成一个代理对象,以后所有对目标对象的操作都会通过代理对象来完成。只有这样通知才可能切入目标对象。对系统的其他部分来说,这个过程是透明的,也就是看起来跟没用代理一样。
我为了简化,只介绍这5个概念。通过这几个概念应该能够理解Spring的切面编程了。如果需要深入了解Spring AOP的话再去学习其他概念也很快的。
下面通过一个实际的例子来说明Spring的切面编程。继续上文Greeting的例子,我们想在Speaker每次说话之前记录Speaker被调用了。
首先创建一个LogAdvice类:
代码片段12:
public class LogAdvice implements MethodBeforeAdvice
{
public void before(Method arg0, Object[] arg1, Object arg2)throws Throwable
{
System.out.println("Speaker called!");
}
}
这里涉及到一个类,MethodBeforeAdvice,这个类是Spring类库提供的,类似的还有AfterReturningAdvice等等,从字面就能理解它们的含义。先不急着理解这个类,我稍后解释。我们继续看如何把这个类应用到我们的系统中去。
代码片段13:
<bean id="Speaker" class="Speaker"/>
<bean id="Greeting" class="Greeting">
<property name="speaker">
<ref bean="SpeakerProxy"/>
<bean id="LogAdvice" class="LogAdvice"/>
<bean id="SpeakerProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces">
ISpeaker
<property name="interceptorNames">
LogAdvice
<property name="target">
<ref local="Speaker"/>
可以看到我们的配置文件中多了两个bean,一个LogAdvice,另外一个SpeakerProxy。LogAdvice很简单。我着重分析一下SpeakerProxy。这个Bean实际上是由Spring提供的ProxyFactoryBean实现。下面定义了三个依赖注入的属性。
1、proxyInterfactes:这个属性定义了这个Proxy要实现哪些接口,可以是一个,也可以是多个(多个的话,要用list标签)。我前面讲过Proxy是在运行是动态创建的,那么这个属性就告诉Spring创建这个Proxy的时候实现哪些接口。
2、interceptorNames:这个属性定义了Proxy被切入了哪些通知,这里只有一个LogAdvice。
3、target:这个属性定义了被代理的对象。在这个例子中target是Speaker。
这样的定义实际上约束了被代理的对象必须实现一个接口,这与上文讲的面向接口的编程有点类似。其实可以这样理解,接口的定义可以让系统的其他部分不受影响,以前用ISpeaker接口来调用,现在加入了Proxy还是一样的。但实际上内容已经不一样了,以前是Speaker,现在是一个Proxy。而target属性让proxy知道具体的方法实现在哪里。Proxy可以看作是target的一个包装。当然Spring并没有强制要求用接口,通过CGLIB(一个高效的代码生成开源类库)也可以直接根据目标对象生成子类,但这种方式并不推荐。
我们还像以前一样的测试我们的Greeting系统,测试代码和代码片段8是一样的。运行结果如下:
Speaker called!
Hello!
看到效果了吧!而且你可以发现,我们加入Log功能并没有改变以前的代码,甚至测试代码都没有改变,这就是AOP的魅力所在!我们更改的只是配置文件。
下面解释一下刚才落下的MethodBeforeAdvice。关于这个类我并不详细介绍,因为这涉及到Spring中的另外一个概念“连接点(Jointpoint)”,我详细介绍一个before这个方法。这个方法有三个参数arg0表示目标对象在哪个点被切入了,既然是MethodBeforeAdvice,那当然是在Method之前被切入了。那么arg0就是表示的那个Method。第二个参数arg1是Method的参数,所以类型是Object[]。第三个参数就是目标对象了,在Greeting例子中arg2的类型实际上是Speaker。
在Greeting例子中,我们并没有指定目标对象的哪些方法要被切入,而是默认切入所有方法调用(虽然Speaker只有一个方法)。通过自定义Pointcut,可以控制切入点,我这里不再介绍了,因为这并不影响理解Spring AOP,有兴趣的话去google一下就知道了。

