(一)核心概念
一、Spring的IoC(Inversion of Control)。
这是Spring中得有特点的一部份。IoC又被翻译成“控制反转”,也不知道是谁翻译得这么别扭,感觉很深奥的词。其实,原理很简单,用一句通俗的话来说:就是用XML来定义生成的对象。IoC其实是一种设计模式,Spring只是实现了这种设计模式。
这种设计模式是怎么来的呢?是实践中逐渐形成的。
第一阶段:用普通的无模式来写Java程序。一般初学者都要经过这个阶段。
第二阶段:频繁的开始使用接口,这时,接口一般都会伴随着使用工厂模式。
第三阶段:使用IoC模式。工厂模式还不够好:(1)因为的类的生成代码写死在程序里,如果你要换一个子类,就要修改工厂方法。(2)一个接口常常意味着一个生成工厂,会多出很多工厂类。
可以把IoC模式看做是工厂模式的升华,可以把IoC看作是一个大工厂,只不过这个大工厂里要生成的对象都是在XML文件中给出定义的,然后利用Java的“反射”编程,根据XML中给出的类名生成相应的对象。从实现来看,IoC是把以前在工厂方法里写死的对象生成代码,改变为由XML文件来定义,也就是把工厂和对象生成这两者独立分隔开来,目的就是提高灵活性和可维护性。
IoC中最基本的Java技术就是“反射”编程。反射又是一个生涩的名词,通俗的说反射就是根据给出的类名(字符串)来生成对象。这种编程方式可以让对象在生成时才决定要生成哪一种对象。我在最近的一个项目也用到了反射,当时是给出一个.properties文本文件,里面写了一些全类名(包名+类名),然后,要根据这些全类名在程序中生成它们的对象。反射的应用是很广泛的,象Hibernate、String中都是用“反射”做为最基本的技术手段。
在过去,反射编程方式相对于正常的对象生成方式要慢10几倍,这也许也是当时为什么反射技术没有普通应用开来的原因。但经SUN改良优化后,反射方式生成对象和通常对象生成方式,速度已经相差不大了(但依然有一倍以上的差距)。
所以要理解IoC,你必须先了解工厂模式和反射编程,否则对它产生的前因后果和实现原理都是无法理解透彻的。只要你理解了这一点,你自己也完全可以自己在程序中实现一个IoC框架,只不是这还要涉及到XML解析等其他知识,稍微麻烦一些。
IoC最大的好处是什么?因为把对象生成放在了XML里定义,所以当我们需要换一个实现子类将会变成很简单(一般这样的对象都是现实于某种接口的),只要修改XML就可以了,这样我们甚至可以实现对象的热插拨(有点象USB接口和SCIS硬盘了)。
IoC最大的缺点是什么?(1)生成一个对象的步骤变复杂了(其实上操作上还是挺简单的),对于不习惯这种方式的人,会觉得有些别扭和不直观。(2)对象生成因为是使用反射编程,在效率上有些损耗。但相对于IoC提高的维护性和灵活性来说,这点损耗是微不足道的,除非某对象的生成对效率要求特别高。(3)缺少IDE重构操作的支持,如果在Eclipse要对类改名,那么你还需要去XML文件里手工去改了,这似乎是所有XML方式的缺憾所在。
总的来说IoC无论原理和实现都还算是很简单的。一些人曾认为IoC没什么实际作用,这种说法是可以理解的,因为如果你在编程中很少使用接口,或很少使用工厂模式,那么你根本就没有使用IoC的强烈需要,也不会体会到IoC可贵之处。有些人也说要消除工厂模式、单例模式,但是都语焉不详、人云亦云。但如果你看到IoC模式和用上Spring,那么工厂模式和单例模式的确基本上可以不用了。但它消失了吗?没有!Spring的IoC实现本身就是一个大工厂,其中也包含了单例对象生成方式,只要用一个设置就可以让对象生成由普通方式变单一实例方式,非常之简单。
总结:
(1)IoC原理很简单,作用的针对性也很强,不要把它看得很玄乎。
(2)要理解IoC,首先要了解“工厂、接口、反射”这些概念。
二、Spring的MVC
如果你已经熟悉Struts,那么不必把MVC做为重点学习内容。基本上我认为Spring MVC是一个鸡肋,它的技术上很先进,但易用性上没有Struts好。而且Struts有这么多年的基础了,Spring很难取代Struts的地位。这就是先入为主的优秀,一个项目经理选用一种框架,不能单纯的从它的技术上考虑,还有开发效率,人员配置等都是考虑因素。但做为研究性的学习,Spring的MVC部份还是蛮有价值的。
三、数据库层的模板
Spring主要是提供了一些数据库模板(模板也是一种Java设计模式),让数据部分的代码更简洁,那些try...catch都可以不见了。这个的确是个好东东。
四、AOP
AOP又称面向方面编程,它的实现原理还是用了反射:通过对某一个种类的方法名做监控来实现统一处理。比如:监控以“insert”字符串开头的方法名,在这种方法执行的前后进行某种处理(数据库事务等)。但这里我有一个疑问?不一定所有以insert开头的方法都是数据库操作,哪么当某个insert开头的方法不是数据库操作,你又对它进行了数据事务的操作,这样的错误如何防止???我对这方面了解不深,还是只知道一个大概。
曾看过一个程序员发出这样的感慨:框架一个接一个,学也学不完,而且有必要吗?这样一层层的加上框架,还不如直接写JSP来得直接,效率还高。我想这种困惑很多人都有吧?但如果你经过的项目渐多,就会发现,维护项目要比开发项目更艰难,代价更大。那种用JSP直接来写,层次又不清楚的开发,往往最后得到一个不可再修改的软件,一团乱麻,移一发而动全身。但软件不象电视机,做好了就不会改动了,软件是一个变化的事物,用户的需求随时会改变,这时你会体会到分层和使用框架的好处了,它们为你做了软件中很多和业务无关的工作,你可以只关注业务,并减少代码量。唯一缺点就是有一个学习的代价,框架配置上也较麻烦。
学习框架,我认为应该:第一步,了解这个框架中的一些关键概念,它的具体含义是什么。第二步,了解这个框架的精华在哪里,它能对开发起到什么样的作用,最好能对它的原理有一定的了解。第三步,用这个框架来写几个例子,实际体会一下。我现在还是刚刚大概完成了前两步,这几天会再看看Spring的文档并用Spring写几个例子,到时一起发出来。
(二)Spring中IoC的入门实例
Spring的模块化是很强的,各个功能模块都是独立的,我们可以选择的使用。这一章先从Spring的IoC开始。所谓IoC就是一个用XML来定义生成对象的模式,我们看看如果来使用的。
1、数据模型。
1、如下图所示有三个类,Human(人类)是接口,Chinese(中国人)是一个子类,American(美国人)是另外一个子类。
源代码如下:
package cn.com.chengang.spring;
public interface Human {
void eat();
void walk();
}
package cn.com.chengang.spring;
public class Chinese implements Human {
/* (非 Javadoc)
* @see cn.com.chengang.spring.Human#eat()
*/
public void eat() {
System.out.println("中国人对吃很有一套");
}
/* (非 Javadoc)
* @see cn.com.chengang.spring.Human#walk()
*/
public void walk() {
System.out.println("中国人行如飞");
}
}
package cn.com.chengang.spring;
public class American implements Human {
/* (非 Javadoc)
* @see cn.com.chengang.spring.Human#eat()
*/
public void eat() {
System.out.println("美国人主要以面包为主");
}
/* (非 Javadoc)
* @see cn.com.chengang.spring.Human#walk()
*/
public void walk() {
System.out.println("美国人以车代步,有四肢退化的趋势");
}
}
2、对以上对象采用工厂模式的用法如下
创建一个工厂类Factory,如下。这个工厂类里定义了两个字符串常量,所标识不同的人种。getHuman方法根据传入参数的字串,来判断要生成什么样的人种。
package cn.com.chengang.spring;
public class Factory {
public final static String CHINESE = "Chinese";
public final static String AMERICAN = "American";
public Human getHuman(String ethnic) {
if (ethnic.equals(CHINESE))
return new Chinese();
else if (ethnic.equals(AMERICAN))
return new American();
else
throw new IllegalArgumentException("参数(人种)错误");
}
}
下面是一个测试的程序,使用工厂方法来得到了不同的“人种对象”,并执行相应的方法。
package cn.com.chengang.spring;
public class ClientTest {
public static void main(String[] args) {
Human human = null;
human = new Factory().getHuman(Factory.CHINESE);
human.eat();
human.walk();
human = new Factory().getHuman(Factory.AMERICAN);
human.eat();
human.walk();
}
}
控制台的打印结果如下:
3、采用Spring的IoC的用法如下:
1、在项目根目录下创建一个bean.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="Chinese" class="cn.com.chengang.spring.Chinese"/>
<bean id="American" class="cn.com.chengang.spring.American"/>
</beans>
bean.xml的位置如下图,注意不要看花眼把它看成是lib目录下的了,它是在myspring目录下的。
2、修改ClientTest程序如下:
package cn.com.chengang.spring;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
public class ClientTest {
public final static String CHINESE = "Chinese";
public final static String AMERICAN = "American";
public static void main(String[] args) {
// Human human = null;
// human = new Factory().getHuman(Factory.CHINESE);
// human.eat();
// human.walk();
// human = new Factory().getHuman(Factory.AMERICAN);
// human.eat();
// human.walk();
ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml");
Human human = null;
human = (Human) ctx.getBean(CHINESE);
human.eat();
human.walk();
human = (Human) ctx.getBean(AMERICAN);
human.eat();
human.walk();
}
}
从这个程序可以看到,ctx就相当于原来的Factory工厂,原来的Factory就可以删除掉了。然后又把Factory里的两个常量移到了ClientTest类里,整个程序结构基本一样。
再回头看原来的bean.xml文件的这一句
<bean id="Chinese" class="cn.com.chengang.spring.Chinese"/>
id就是ctx.getBean的参数值,一个字符串。class就是一个类(包名+类名)。然后在ClientTest类里获得Chinese对象就是这么一句
human = (Human) ctx.getBean(CHINESE);
因为getBean方法返回的是Object类型,所以前面要加一个类型转换。
4、总结
(1)也许有人说,IoC和工厂模式不是一样的作用吗,用IoC好象还麻烦一点。
举个例子,如果用户需求发生变化,要把Chinese类修改一下。那么前一种工厂模式,就要更改Factory类的方法,并且重新编译布署。而IoC只需要将class属性改变一下,并且由于IoC利用了Java反射机制,这些对象是动态生成的,这时我们就可以热插拨Chinese对象(不必把原程序停止下来重新编译布署)
(2)也许有人说,即然IoC这么好,那么我把系统所有对象都用IoC方式来生成。
注意,IoC的灵活性是有代价的:设置步骤麻烦、生成对象的方式不直观、反射比正常生成对象在效率上慢一点。因此使用IoC要看有没有必要,我认为比较通用的判断方式是:用到工厂模式的地方都可以考虑用IoC模式。
(3)在上面的IoC的方式里,还有一些可以变化的地方。比如,bean.xml不一定要放在项目录下,也可以放在其他地方,比如cn.com.chengang.spring包里。不过在使用时也要变化一下,如下所示:
new FileSystemXmlApplicationContext("src/cn/com/chengang/spring/bean.xml");
另外,bean.xml也可以改成其他名字。这样我们在系统中就可以分门别类的设置不同的bean.xml。
(4)关于IoC的低侵入性。
什么是低侵入性?如果你用过Struts或EJB就会发现,要继承一些接口或类,才能利用它们的框架开发。这样,系统就被绑定在Struts、EJB上了,对系统的可移植性产生不利的影响。如果代码中很少涉及某一个框架的代码,那么这个框架就可以称做是一个低侵入性的框架。
Spring的侵入性很低,Humen.java、Chinese.java等几个类都不必继承什么接口或类。但在ClientTest里还是有一些Spring的影子:FileSystemXmlApplicationContext类和ctx.getBean方式等。
现在,低侵入性似乎也成了判定一个框架的实现技术好坏的标准之一。
(5)关于bean.xml的用法
bean.xml的用法还有很多,其中内容是相当丰富的。假设Chinese类里有一个humenName属性(姓名),那么原的bean.xml修改如下。此后生成Chinese对象时,“陈刚”这个值将自动设置到Chinese类的humenName属性中。而且由于singleton为true这时生成Chinese对象将采用单例模式,系统仅存在一个Chinese对象实例。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="Chinese" class="cn.com.chengang.spring.Chinese" singleton="true">
<property name="humenName">
<value>陈刚</value>
</property>
</bean>
<bean id="American" class="cn.com.chengang.spring.American"/>
</beans>
(三)IoC中的国际化(CVS版本:V002)
3.1 前言
标题准确来说应该是“使用Spring中的IoC功能来实现我们所开发项目系统的国际化”,国际化不是针对IoC的,而是针对你开发的整个系统。
如果你使用过Eclipse的国际化,或者用过Eclipse的“外部化字符串”向导(Eclipse主菜单:源代码->外部化字符串),那么对Spring提供的国际化功能应该是非常容易理解,两者基本一样,或者说各种Java程序的国际化方式都基本一样。
先谈谈Eclipse国际化的两个组成部分:*.properties的资源文件、获取资源文件内容的Message类。
而Spring则和Eclipse的处理类似:资源文件两者是一样的,不同语言的翻译放在不同的资源文件里,连起名规则都一样;Eclipse的Message类要自己写(代码通用,复制以前项目的即可,或用Eclipse的向导生成一个也行),Spring则已经有写好的Message类,我们在IoC的xml文件里注册一下即可使用(也可以实现Spring的MessageSource接口,自己来写一个Message类,代码并不复杂,不过这没什么必要,用Spring提供的就行了)。
无论是Eclipse的Message类,还是Spring的自带的Message类,或是我们自己写一个Message类,都是使用JDK的java.util.ResourceBundle类来实现*.properties文件的读取。
下面用实例来体会一下,先给出本章完成之后的项目结构的截图:
3.2 简单实例
假设我们有如下程序,程序的作用是打印出一个字符串
package cn.com.chengang.spring;
public class MessageTest {
public static void main(String[] args) {
String str = "ChenGang";
System.out.println(str);
}
}
现在,我们要让这个程序能够根据使用者的语言情况输出不同的字符,比如:对英文使用者输出“ChenGang”,对中文使用者输出“陈刚”,对台湾使用输出“陳剛”等等。这个需求的实现方法如下:
1、创建一系列的资源文件
在cn.com.chengang.spring包下创建以下文件:
(1)messages.properties(默认:英文),内容仅一句,如下
chengang=Giles
“chengang”是键值,Giles是要输出的英文字符串
(2)messages_zh_CN.properties(简体中文)
chengang=/u9648/u521A
“/u9648/u521A”是UNICODE码,对应的中文是“陈刚”
(3)messages_ zh_TW.properties(繁体中文)
chengang=/u9673/u525B
“/u9673/u525B”对应的中文是“陳剛”
附注:由于中文是要转换成UNICODE码,在编辑和阅读上有诸多不便,如果是用Eclipse做IDE,则有一个编辑资源文件的插件jinto,下载网址是http://www.guh-software.de/,用它打开的资源文件如下图所示,可以看到三个资源在一个界面反映了出来。
如果你不用Eclipse,而是用Editplugs+JDK的方式来编程(现在还有这样的原始人吗?),你也可以用JDK自带的native2ascii.exe程序来将中文字串转成UNICODE码。Ant中还提供了一个相应的任务:<native2ascii encoding="GBK" src="${src}" dest="${build}"/>,其中GBK是一个中国的字符集。
2、修改bean.xml
将Spring自带的org.springframework.context.support.ResourceBundleMessageSource类注册到bean.xml中,这个类的作用是获取资源文件的内容,注册到IoC的bean.xml文件中是为了自动获得此类的对象(Spring做了一些简化编程的处理)。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="Chinese" class="cn.com.chengang.spring.Chinese"/>
<bean id="American" class="cn.com.chengang.spring.American"/>
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>cn.com.chengang.spring.messages</value>
</list>
</property>
</bean>
</beans>
代码说明:
l id="messageSource" 的设置是不变的、必须的。
l ResourceBundleMessageSource是Spring的一个Message类。这里还有一个选择,用ReloadableResourceBundleMessageSource类,此类可以提供不用重启即可重新加载资源文件的特性(前者对资源文件只加载一次)。对于那种有热修改资源文件的需求,后者比较合适,只是后者在效率上有可能有损耗,因为至少要多一些检查资源文件是否改变的代码(这只是我的猜测,我没有仔佃去读这段的源码)。
l “basenames”是不变的、必须的。它是ResourceBundleMessageSource的一个属性,在源代码中的定义是“private String[] basenames;”,可见它是一个字符串数组。
l “cn.com.chengang.spring.messages”是把资源文件的位置传入到basenames属性中。注意:三个资源文件只需要将共同的主名(红色字体)传入:messages.properties、messages_zh_CN.properties、messages_zh_TW.properties。
3、使用。修改MessageTest类,如下
package cn.com.chengang.spring;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
public class MessageTest {
public static void main(String[] args) {
ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml");
String str = ctx.getMessage("chengang", null, null);
System.out.println(str);
}
}
代码说明:
(1)main方法里
l 第一句取得bean.xml文件的配置信息。
l 第二句从资源文件里得到键值chengang对应的字符串。
l 第三句将字符串打印出来,结果是打印的是“陈刚”,说明读取的是messages_zh_CN.properties资源文件。
(2)ctx.getMessage("chengang", null, null);有三个参数:
l 第一个是资源文件的键值;
l 第二个是资源文件字符串的参数,由于本字符串没有参数,所以用一个null(后面给出了一个用到字符串参数的实例);
l 第三个是一个java.util. Locale类型的参数。参数为null,则表示根据使用者的语言环境来选择Locale,因为我用的是中文版的windows,所以在取字符串时它自动选择了messages_zh_CN.properties资源文件。
这其中还有一个控制点在JVM,JVM会根据当前操作系统的语言环境进行相应处理,我们可以通过在JVM启动参数中追加“-Duser.language=zh_TW”来设定当前JVM语言类型,通过JVM级的设定,也可以实现自动切换所使用的资源文件类型。
所以这里面的控制语言的方式有三种:从最低层的操作系统的Locale设定,到更上一层的JVM的Locale设定,再到程序一级的Locale设定。
3.3 资源文件的其他使用方式:
package cn.com.chengang.spring;
import java.util.Locale;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.context.support.ResourceBundleMessageSource;
public class MessageTest {
public static void main(String[] args) {
ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml");
String str = ctx.getMessage("chengang", null, null);
System.out.println(str); //输出“陈刚”
/*
* 使用了messages.properties
*/
str = ctx.getMessage("chengang", null, new Locale(""));
System.out.println(str);//输出“Giles”
/*
* 使用了messages_zh_CN.properties
*/
str = ctx.getMessage("chengang", null, new Locale("zh", "CN"));
System.out.println(str);//输出“陈刚”
/*
* 使用了messages_zh_TW.properties
*/
str = ctx.getMessage("chengang", null, new Locale("zh", "TW"));
System.out.println(str);//输出“陳剛”
/*
* 使用了messages_zh_TW.properties,从这里可见资源文件的起名可以很随意,
* 比如我们建立一个messages_123.properties,在传参数时候就可以这样:
* new Locale("123"),一样也可以取出messages_123.properties中的值
*/
str = ctx.getMessage("chengang", null, new Locale("zh_TW"));
System.out.println(str);//输出“陳剛”
/*
* 当找不到相应的资源文件时,使用了messages_zh_CN.properties
*/
str = ctx.getMessage("chengang", null, new Locale("abcd"));
System.out.println(str);//输出“陈刚”
/**
* 不通过IoC注册,直接使用ResourceBundleMessageSource类的写法。
*/
ResourceBundleMessageSource s = new ResourceBundleMessageSource();
s.setBasename("cn.com.chengang.spring.messages");
str = s.getMessage("chengang", null, null);
System.out.println(str);//输出“陈刚”
}
}
代码说明:
前面说过控制语言的方式有三种:从最低层的操作系统的Locale设定,到更上一层的JVM的Locale设定,再到程序一级的Locale设定。我认为最佳的方法是在程序一级进行控制:定义一个统一的Locale静态变量,然后整个系统中只使用这一个变量,以后就可以通过界面操作设置此Locale变量的值,让用户来选择他所需的软件语言。而且我们也可以将此静态变量设成null值,来自动选择资源文件。
另外,Locale里也定义了一些常量,我们可以直接使用而不必去new一个Locale对象,如:“Locale.ENGLISH”。
3.4 再一个实例
这个实例演示了如何使用多个资源文件,以及如何使用字符串参数
(1)在cn.com.chengang.spring包下再创建一个资源文件messagesOther_zh_CN.properties
chengang.info=/u9648/u521A/uFF0C/u7F51/u540D/uFF1A{0}/uFF0C/u82F1/u6587/u540D/uFF1A{1}/uFF0CBlog/uFF1A{2}
其中UNICODE字符串对应的中文是:“陈刚,网名:{0},英文名:{1},Blog:{2}”,这个字符串一共有三个参数。
(2)修改 bean.xml文件
因为basenames属性是一个数组,当然也就可以接收多个资源文件设定。具体修改如下面的红字部份
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="Chinese" class="cn.com.chengang.spring.Chinese"/>
<bean id="American" class="cn.com.chengang.spring.American"/>
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>cn.com.chengang.spring.messages</value>
<value>cn.com.chengang.spring.messagesOther</value>
</list>
</property>
</bean>
</beans>
(3)修改MessageTest类,加入几行使用的代码
String[] strArgs = new String[3];
strArgs[0]="混北民工";
strArgs[1]="Giles";
strArgs[2]="http://blog.csdn.net/glchengang";
str = ctx.getMessage("chengang.info", strArgs, null);
System.out.println(str);
打印出来的结果就是:“陈刚,网名:混北民工,英文名:Giles,Blog:http://blog.csdn.net/glchengang”
3.5 国际化的实践建议
l 建议一个包对应一个资源文件。不要整个系统都使用一个资源文件来翻译,这样单个文档的体积就太大了,不利于维护;当然,也不必一个类对应一个资源文件,这样资源文件又太多了。
l 建议资源文件和其翻译类/包在同一目录下。不过,如果是要将软件打成一外JAR包或WAR包,建议把资源文件分离出来,这样可以修改资源文件,而不必再次打包。
l 建议字符串项的键值上加上其所在的类名。比如:上面的chengang和chengang.info最好是取名成MessageTest.chengang和MessageTest.chengang.info。这样查找使用此键值的类会方便很多。
(四)学习方法与问题
1、如何学习Spring? 转chensheng913 的 Blog
你可以通过下列途径学习spring:
(1) spring下载包中doc目录下的MVC-step-by-step和sample目录下的例子都是比较好的spring开发的例子。
(2) AppFuse集成了目前最流行的几个开源轻量级框架或者工具 Ant,XDoclet,Spring,Hibernate(iBATIS),JUnit,Cactus,StrutsTestCase,Canoo's WebTest,Struts Menu,Display Tag Library,OSCache,JSTL,Struts 。
你可以通过AppFuse源代码来学习spring。
AppFuse网站:http://raibledesigns.com/wiki/Wiki.jsp?page=AppFuse
(3)Spring 开发指南(夏昕)(http://www.xiaxin.net/Spring_Dev_Guide.rar)
一本spring的入门书籍,里面介绍了反转控制和依赖注射的概念,以及spring的bean管理,spring的MVC,spring和hibernte,iBatis的结合。
(4) spring学习的中文论坛
SpringFramework中文论坛(http://spring.jactiongroup.net)
Java视线论坛(http://forum.javaeye.com)的spring栏目
2、利用Spring框架编程,console打印出log4j:WARN Please initialize the log4j system properly?
说明你的log4j.properties没有配置。请把log4j.properties放到工程的classpath中,eclipse的classpath为bin目录,由于编译后src目录下的文件会拷贝到bin目录下,所以你可以把log4j.properties放到src目录下。
这里给出一个log4j.properties的例子:
log4j.rootLogger=DEBUG,stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d %5p (%F:%L) - %m%n |
3、出现 java.lang.NoClassDefFoundError?
一般情况下是由于你没有把必要的jar包放到lib中。
比如你要采用spring和hibernate(带事务支持的话),你除了spring.jar外还需要hibernat.jar、aopalliance.jar、cglig.jar、jakarta-commons下的几个jar包。
http://www.springframework.org/download.html下载spring开发包,提供两种zip包
spring-framework-1.1.3-with-dependencies.zip和spring-framework-1.1.3.zip,我建议你下载spring-framework-1.1.3-with-dependencies.zip。这个zip解压缩后比后者多一个lib目录,其中有hibernate、j2ee、dom4j、aopalliance、jakarta-commons等常用包。
4、java.io.FileNotFoundException: Could not open class path resource [....hbm.xml],提示找不到xml文件?
原因一般有两个:
(1)该xml文件没有在classpath中。
(2)applicationContext-hibernate.xml中的xml名字没有带包名。比如:
<bean id="sessionFactory" class="org.springframework.orm.hibernate.LocalSessionFactoryBean"> <property name="dataSource"><ref bean="dataSource"/></property> <property name="mappingResources"> <list> <value>User.hbm.xml</value> 错,改为: <value>com/yz/spring/domain/User.hbm.xml</value> </list> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect"> net.sf.hibernate.dialect.MySQLDialect </prop> <prop key="hibernate.show_sql">true</prop> </props> </property> </bean> |
5、org.springframework.beans.NotWritablePropertyException: Invalid property 'postDao' of bean class?
出现异常的原因是在application-xxx.xml中property name的错误。
<property name="...."> 中name的名字是与bean的set方法相关的,而且要注意大小写。
比如
public class PostManageImpl extends BaseManage implements PostManage { private PostDAO dao = null; public void setPostDAO(PostDAO postDAO){ this.dao = postDAO; } } |
那么xml的定义应该是:
<bean id="postManage" parent="txProxyTemplate"> <property name="target"> <bean class="com.yz.spring.service.implement.PostManageImpl"> <property name="postDAO"><ref bean="postDAO"/></property> 对 <property name="dao"><ref bean="postDAO"/></property> 错 </bean> </property> </bean> |
6、Spring中如何实现事务管理?
首先,如果使用mysql,确定mysql为InnoDB类型。
事务管理的控制应该放到商业逻辑层。你可以写个处理商业逻辑的JavaBean,在该JavaBean中调用DAO,然后把该Bean的方法纳入spring的事务管理。
比如:xml文件定义如下:
<bean id="txProxyTemplate" abstract="true" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager"><ref bean="transactionManager"/></property> <property name="transactionAttributes"> <props> <prop key="save*">PROPAGATION_REQUIRED</prop> <prop key="remove*">PROPAGATION_REQUIRED</prop> <prop key="*">PROPAGATION_REQUIRED</prop> </props> </property> </bean> <bean id="userManage" parent="txProxyTemplate"> <property name="target"> <bean class="com.yz.spring.service.implement.UserManageImpl"> <property name="userDAO"><ref bean="userDAO"/></property> </bean> </property> </bean> |
com.yz.spring.service.implement.UserManageImpl就是我们的实现商业逻辑的JavaBean。我们通过parent元素声明其事务支持。
7、如何管理Spring框架下更多的JavaBean?
JavaBean越多,spring配置文件就越大,这样不易维护。为了使配置清晰,我们可以将JavaBean分类管理,放在不同的配置文件中。 应用启动时将所有的xml同时加载。
比如:
DAO层的JavaBean放到applicationContext-hibernate.xml中,商业逻辑层的JavaBean放到applicationContext-service.xml中。然后启动类中调用以下代码载入所有的ApplicationContext。
String[] paths = {"com/yz/spring/dao/hibernate/applicationContext-hibernate.xml", "com/yz/spring/service/applicationContext-service.xml"}; ctx = new ClassPathXmlApplicationContext(paths); |
8、web应用中如何加载ApplicationContext?
可以通过定义web.xml,由web容器自动加载。
<servlet> <servlet-name>context</servlet-name> <servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/applicationContext-hibernate.xml</param-value> <param-value>/WEB-INF/applicationContext-service.xml</param-value> </context-param> |
9、在spring中如何配置的log4j?
在web.xml中加入以下代码即可。
<context-param> <param-name>log4jConfigLocation</param-name> <param-value>/WEB-INF/log4j.properties</param-value> </context-param> |
10、Spring框架入门的编程问题解决了,我该如何更深地领会Spring框架呢?
这两本书你该去看看。这两本书是由Spring的作者Rod Johnson编写的。
Expert One on one J2EE Design and Development Expert One on one J2EE Development Without EJB |
你也该看看martinfowler的Inversion of Control Containers and the Dependency Injection pattern。
http://www.martinfowler.com/articles/injection.html |
再好好研读一下spring的文档。
http://www.jactiongroup.net/reference/html/index.html(中文版,未全部翻译) |
(五)Spring AOP: Spring之面向方面编程
AOP正在成为软件开发的下一个圣杯。使用AOP,你可以将处理aspect的代码注入主程序,通常主程序的主要目的并不在于处理这些aspect。AOP可以防止代码混乱。
为了理解AOP如何做到这点,考虑一下记日志的工作。日志本身不太可能是你开发的主程序的主要任务。如果能将“不可见的”、通用的日志代码注入主程序中,那该多好啊。AOP可以帮助你做到。
Spring framework是很有前途的AOP技术。作为一种非侵略性的,轻型的AOP framework,你无需使用预编译器或其他的元标签,便可以在Java程序中使用它。这意味着开发团队里只需一人要对付AOP framework,其他人还是象往常一样编程。
AOP是很多直觉难以理解的术语的根源。幸运的是,你只要理解三个概念,就可以编写AOP模块。这三个概念是:advice,pointcut和advisor。advice是你想向别的程序内部不同的地方注入的代码。pointcut定义了需要注入advice的位置,通常是某个特定的类的一个public方法。advisor是pointcut和advice的装配器,是将advice注入主程序中预定义位置的代码。
既然我们知道了需要使用advisor向主要代码中注入“不可见的”advice,让我们实现一个Spring AOP的例子。在这个例子中,我们将实现一个before advice,这意味着advice的代码在被调用的public方法开始前被执行。以下是这个before advice的实现代码:
代码: |
package com.company.springaop.test; import java.lang.reflect.Method; import org.springframework.aop.MethodBeforeAdvice; public class TestBeforeAdvice implements MethodBeforeAdvice { public void before(Method m, Object[] args, Object target) throws Throwable { System.out.println("Hello world! (by " + this.getClass().getName() + ")"); } } |
接口MethodBeforeAdvice只有一个方法before需要实现,它定义了advice的实现。before方法共用三个参数,它们提供了相当丰富的信息。参数Method m是advice开始后执行的方法。方法名称可以用作判断是否执行代码的条件。Object[] args是传给被调用的public方法的参数数组。当需要记日志时,参数args和被执行方法的名称,都是非常有用的信息。你也可以改变传给m的参数,但要小心使用这个功能;编写最初主程序的程序员并不知道主程序可能会和传入参数的发生冲突。Object target是执行方法m对象的引用。
在下面的BeanImpl类中,每个public方法调用前,都会执行advice:
代码: |
package com.company.springaop.test; public class BeanImpl implements Bean { public void theMethod() { System.out.println(this.getClass().getName() + "." + new Exception().getStackTrace()[0].getMethodName() + "()" + " says HELLO!"); } } |
类BeanImpl实现了下面的接口Bean:
代码: |
package com.company.springaop.test; public interface Bean { public void theMethod(); } |
虽然不是必须使用接口,但面向接口而不是面向实现编程是良好的编程实践,Spring也鼓励这样做。
pointcut和advice通过配置文件来实现,因此,接下来你只需编写主方法的Java代码:
代码: |
package com.company.springaop.test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; public class Main { public static void main(String[] args) { //Read the configuration file ApplicationContext ctx = new FileSystemXmlApplicationContext("springconfig.xml"); //Instantiate an object Bean x = (Bean) ctx.getBean("bean"); //Execute the public method of the bean (the test) x.theMethod(); } } |
我们从读入和处理配置文件开始,接下来马上要创建它。这个配置文件将作为粘合程序不同部分的“胶水”。读入和处理配置文件后,我们会得到一个创建工厂ctx。任何一个Spring管理的对象都必须通过这个工厂来创建。对象通过工厂创建后便可正常使用。
仅仅用配置文件便可把程序的每一部分组装起来。
代码: |
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <!--CONFIG--> <bean id="bean" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces"> <value>com.company.springaop.test.Bean</value> </property> <property name="target"> <ref local="beanTarget"/> </property> <property name="interceptorNames"> <list> <value>theAdvisor</value> </list> </property> </bean> <!--CLASS--> <bean id="beanTarget" class="com.company.springaop.test.BeanImpl"/> <!--ADVISOR--> <!--Note: An advisor assembles pointcut and advice--> <bean id="theAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <property name="advice"> <ref local="theBeforeAdvice"/> </property> <property name="pattern"> <value>com/.company/.springaop/.test/.Bean/.theMethod</value> </property> </bean> <!--ADVICE--> <bean id="theBeforeAdvice" class="com.company.springaop.test.TestBeforeAdvice"/> </beans> |
四个bean定义的次序并不重要。我们现在有了一个advice,一个包含了正则表达式pointcut的advisor,一个主程序类和一个配置好的接口,通过工厂ctx,这个接口返回自己本身实现的一个引用。
BeanImpl和TestBeforeAdvice都是直接配置。我们用一个唯一的ID创建一个bean元素,并指定了一个实现类。这就是全部的工作。
advisor通过Spring framework提供的一个RegexMethodPointcutAdvisor类来实现。我们用advisor的一个属性来指定它所需的advice-bean。第二个属性则用正则表达式定义了pointcut,确保良好的性能和易读性。
最后配置的是bean,它可以通过一个工厂来创建。bean的定义看起来比实际上要复杂。bean是ProxyFactoryBean的一个实现,它是Spring framework的一部分。这个bean的行为通过一下的三个属性来定义:
- 属性proxyInterface定义了接口类。
- 属性target指向本地配置的一个bean,这个bean返回一个接口的实现。
- 属性interceptorNames是唯一允许定义一个值列表的属性。这个列表包含所有需要在beanTarget上执行的advisor。注意,advisor列表的次序是非常重要的。
Spring工具
虽然你可以手工修改Ant构建脚本,但使用SpringUI(译注:SpringUI现在是Spring framework的一部分,并改名为spring-ide),使用Spring AOP变得很简单,只要点点鼠标即可。你可以把SpringUI安装成Eclipse的一个plug-in。然后,你只需在你的project上右击鼠标,并选择“add Spring Project Nature”。在project属性中,你可以在“Spring Project”下添加Spring配置文件。在编译前把下面的类库加入project:aopalliance.jar,commons-logging.jar,jakarta-oro-2.0.7.jar和spring.jar。运行程序时你会看到下面的信息:
... (logging information)
Hello world! (by com.company.springaop.test.TestBeforeAdvice)
com.company.springaop.test.BeanImpl.theMethod() says HELLO!
优点和缺点
Spring比起其他的framework更有优势,因为除了AOP以外,它提供了更多别的功能。作为一个轻型framework,它在J2EE不同的部分都可以发挥作用。因此,即使不想使用Spring AOP,你可能还是想使用Spring。另一个优点是,Spring并不要求开发团队所有的人员都会用它。学习Spring应该从Spring reference的第一页开始。读了本文后,你应该可以更好地理解Spring reference了。Spring唯一的缺点是缺乏更多的文档,但它的mailing list是个很好的补充,而且会不断地出现更多的文档。
- 属性proxyInterface定义了接口类。
- 属性target指向本地配置的一个bean,这个bean返回一个接口的实现。
- 属性interceptorNames是唯一允许定义一个值列表的属性。这个列表包含所有需要在beanTarget上执行的advisor。注意,advisor列表的次序是非常重要的。
Spring工具
虽然你可以手工修改Ant构建脚本,但使用SpringUI(译注:SpringUI现在是Spring framework的一部分,并改名为spring-ide),使用Spring AOP变得很简单,只要点点鼠标即可。你可以把SpringUI安装成Eclipse的一个plug-in。然后,你只需在你的project上右击鼠标,并选择“add Spring Project Nature”。在project属性中,你可以在“Spring Project”下添加Spring配置文件。在编译前把下面的类库加入project:aopalliance.jar,commons-logging.jar,jakarta-oro-2.0.7.jar和spring.jar。运行程序时你会看到下面的信息:
... (logging information)
Hello world! (by com.company.springaop.test.TestBeforeAdvice)
com.company.springaop.test.BeanImpl.theMethod() says HELLO!
优点和缺点
Spring比起其他的framework更有优势,因为除了AOP以外,它提供了更多别的功能。作为一个轻型framework,它在J2EE不同的部分都可以发挥作用。因此,即使不想使用Spring AOP,你可能还是想使用Spring。另一个优点是,Spring并不要求开发团队所有的人员都会用它。学习Spring应该从Spring reference的第一页开始。读了本文后,你应该可以更好地理解Spring reference了。Spring唯一的缺点是缺乏更多的文档,但它的mailing list是个很好的补充,而且会不断地出现更多的文档。
面向方面编程 (AOP) 提供从另一个角度来考虑程序结构以完善面向对象编程(OOP)。 面向对象将应用程序分解成 各个层次的对象,而AOP将程序分解成各个方面 或者说 关注点 。 这使得可以模块化诸如事务管理等这些横切多个对象的关注点。(这些关注点术语称作 横切关注点。)
Spring的一个关键组件就是AOP框架。 Spring IoC容器(BeanFactory 和ApplicationContext)并不依赖于AOP, 这意味着如果你不需要使用,AOP你可以不用,AOP完善了Spring IoC,使之成为一个有效的中间件解决方案,。
AOP在Spring中的使用:
提供声明式企业服务,特别是作为EJB声明式服务的替代品。这些服务中最重要的是 声明式事务管理,这个服务建立在Spring的事务管理抽象之上。
允许用户实现自定义的方面,用AOP完善他们的OOP的使用。
这样你可以把Spring AOP看作是对Spring的补充,它使得Spring不需要EJB就能提供声明式事务管理;或者 使用Spring AOP框架的全部功能来实现自定义的方面。
如果你只使用通用的声明式服务或者预先打包的声明式中间件服务如pooling,你可以不直接使用 Spring AOP,并且跳过本章的大部分内容.让我们从定义一些重要的AOP概念开始。这些术语不是Spring特有的。不幸的是,Spring的术语 不是特别地直观。而且,如果Spring使用自己的术语,这将使人更加迷惑。
方面(Aspect): 一个关注点的模块化,这个关注点实现可能 另外横切多个对象。事务管理是J2EE应用中一个很好的横切关注点例子。方面用Spring的 Advisor或拦截器实现。
连接点(Joinpoint): 程序执行过程中明确的点,如方法的调 用或特定的异常被抛出。
通知(Advice): 在特定的连接点,AOP框架执行的动作。各种类 型的通知包括“around”、“before”和“throws”通知。通知类型将在下面讨论。许多AOP框架 包括Spring都是以拦截器做通知模型,维护一个“围绕”连接点的拦截器 链。
切入点(Pointcut): 指定一个通知将被引发的一系列连接点 的集合。AOP框架必须允许开发者指定切入点:例如,使用正则表达式。
引入(Introduction): 添加方法或字段到被通知的类。 Spring允许引入新的接口到任何被通知的对象。例如,你可以使用一个引入使任何对象实现 IsModified接口,来简化缓存。
目标对象(Target Object): 包含连接点的对象。也被称作 被通知或被代理对象。
AOP代理(AOP Proxy): AOP框架创建的对象,包含通知。 在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
织入(Weaving): 组装方面来创建一个被通知对象。这可以在编译时 完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样, 在运行时完成织入。
各种通知类型包括:
Around通知: 包围一个连接点的通知,如方法调用。这是最 强大的通知。Aroud通知在方法调用前后完成自定义的行为。它们负责选择继续执行连接点或通过 返回它们自己的返回值或抛出异常来短路执行。
Before通知: 在一个连接点之前执行的通知,但这个通知 不能阻止连接点前的执行(除非它抛出一个异常)。
Throws通知: 在方法抛出异常时执行的通知。Spring提供 强类型的Throws通知,因此你可以书写代码捕获感兴趣的异常(和它的子类),不需要从Throwable 或Exception强制类型转换。
After returning通知: 在连接点正常完成后执行的通知, 例如,一个方法正常返回,没有抛出异常。
Around通知是最通用的通知类型。大部分基于拦截的AOP框架,如Nanning和JBoss4,只提供 Around通知。
如同AspectJ,Spring提供所有类型的通知,我们推荐你使用最为合适的通知类型来实现需 要的行为。例如,如果只是需要用一个方法的返回值来更新缓存,你最好实现一个after returning 通知而不是around通知,虽然around通知也能完成同样的事情。使用最合适的通知类型使编程模型变 得简单,并能减少潜在错误。例如你不需要调用在around通知中所需使用的的MethodInvocation的 proceed()方法,因此就调用失败。
切入点的概念是AOP的关键,使AOP区别于其它使用拦截的技术。切入点使通知独立于OO的 层次选定目标。例如,提供声明式事务管理的around通知可以被应用到跨越多个对象的一组方法上。 因此切入点构成了AOP的结构要素。
Spring AOP用纯Java实现。它不需要特别的编译过程。Spring AOP不需要控制类装载器层次, 因此适用于J2EE web容器或应用服务器。
Spring目前支持拦截方法调用。成员变量拦截器没有实现,虽然加入成员变量拦截器支持并不破坏 Spring AOP核心API。
成员变量拦截器在违反OO封装原则方面存在争论。我们不认为这在应用程序开发中是明智的。如 果你需要使用成员变量拦截器,考虑使用AspectJ。Spring提供代表切入点或各种通知类型的类。Spring使用术语advisor来 表示代表方面的对象,它包含一个通知和一个指定特定连接点的切入点。
各种通知类型有MethodInterceptor (来自AOP联盟的拦截器API)和定义在org.springframework.aop包中的 通知接口。所有通知必须实现org.aopalliance.aop.Advice标签接口。 取出就可使用的通知有 MethodInterceptor、 ThrowsAdvice、 BeforeAdvice和 AfterReturningAdvice。我们将在下面详细讨论这些通知类型。
Spring实现了AOP联盟的拦截器接口( http://www.sourceforge.net/projects/aopalliance). Around通知必须实现AOP联盟的org.aopalliance.intercept.MethodInterceptor 接口。这个接口的实现可以运行在Spring或其他AOP联盟兼容的实现中。目前JAC实现了AOP联盟的接 口,Nanning和Dynaop可能在2004年早期实现。
Spring实现AOP的途径不同于其他大部分AOP框架。它的目标不是提供及其完善的AOP实现( 虽然Spring AOP非常强大);而是提供一个和Spring IoC紧密整合的AOP实现,帮助解决企业应用 中的常见问题。 因此,例如Spring AOP的功能通常是和Spring IoC容器联合使用的。AOP通知是用普通 的bean定义语法来定义的(虽然可以使用"autoproxying"功能);通知和切入点本身由Spring IoC 管理:这是一个重要的其他AOP实现的区别。有些事使用Spring AOP是无法容易或高效地实现,比如通知 非常细粒度的对象。这种情况AspectJ可能是最合适的选择。但是,我们的经验是Spring针对J2EE应 用中大部分能用AOP解决的问题提供了一个优秀的解决方案。让我们看看Spring如何处理切入点这个重要的概念。
Spring的切入点模型能够使切入点独立于通知类型被重用。 同样的切入点有可能接受不同的 通知。
org.springframework.aop.Pointcut 接口是重要的接口, 用来指定通知到特定的类和方法目标。完整的接口定义如下:
public interface Pointcut { ClassFilter getClassFilter(); MethodMatcher getMethodMatcher(); }
将Pointcut接口分成两个部分有利于重用类和方法的匹配部分,并且组合细粒度的 操作(如和另一个方法匹配器执行一个”并“的操作)。
ClassFilter接口被用来将切入点限制到一个给定的目标类的集合。 如果matches()永远返回true,所有的目标类都将被匹配。
public interface ClassFilter { boolean matches(Class clazz); }
MethodMatcher接口通常更加重要。完整的接口如下:
public interface MethodMatcher { boolean matches(Method m, Class targetClass); boolean isRuntime(); boolean matches(Method m, Class targetClass, Object[] args); }
matches(Method, Class) 方法被用来测试这个切入点是否匹 配目标类的给定方法。这个测试可以在AOP代理创建的时候执行,避免在所有方法调用时都需要进行 测试。如果2个参数的匹配方法对某个方法返回true,并且MethodMatcher的 isRuntime()也返回true,那么3个参数的匹配方法将在每次方法调用的时候被调用。这使 切入点能够在目标通知被执行之前立即查看传递给方法调用的参数。
大部分MethodMatcher都是静态的,意味着isRuntime()方法 返回false。这种情况下3个参数的匹配方法永远不会被调用。
如果可能,尽量使切入点是静态的,使当AOP代理被创建时,AOP框架能够缓存切入点的 测试结果。Spring支持的切入点的运算有: 值得注意的是 并 和 交。
并表示只要任何一个切入点匹配的方法。
交表示两个切入点都要匹配的方法。
并通常比较有用。
切入点可以用org.springframework.aop.support.Pointcuts 类的静态方法来组合,或者使用同一个包中的ComposablePointcut类。
Spring提供几个实用的切入点实现。一些可以直接使用。另一些需要子类化来实现应用相 关的切入点。
静态切入点只基于方法和目标类,而不考虑方法的参数。静态切入点足够满足大多数情况 的使用。Spring可以只在方法第一次被调用的时候计算静态切入点,不需要在每次方法调用 的时候计算。
让我们看一下Spring提供的一些静态切入点的实现。
一个很显然的指定静态切入点的方法是正则表达式。除了Spring以外,其它的AOP框架也实 现了这一点。org.springframework.aop.support.RegexpMethodPointcut 是一个通用的正则表达式切入点,它使用Perl 5的正则表达式的语法。
使用这个类你可以定义一个模式的列表。如果任何一个匹配,那个切入点将被计算成 true。(所以结果相当于是这些切入点的并集)。
用法如下:
<bean id="settersAndAbsquatulatePointcut" class="org.springframework.aop.support.RegexpMethodPointcut"> <property name="patterns"> <list> <value>.*get.*</value> <value>.*absquatulate</value> </list> </property> </bean>
RegexpMethodPointcut一个实用子类, RegexpMethodPointcutAdvisor, 允许我们同时引用一个通知。 (记住通知可以是拦截器,before通知,throws通知等等。)这简化了bean的装配,因为一个bean 可以同时当作切入点和通知,如下所示:
<bean id="settersAndAbsquatulateAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <property name="interceptor"> <ref local="beanNameOfAopAllianceInterceptor"/> </property> <property name="patterns"> <list> <value>.*get.*</value> <value>.*absquatulate</value> </list> </property> </bean>
RegexpMethodPointcutAdvisor可以用于任何通知类型。
RegexpMethodPointcut类需要Jakarta ORO正则表达式包。动态切入点的演算代价比静态切入点高的多。它们不仅考虑静态信息,还要考虑方法的 参数。这意味着它们必须在每次方法调用的时候都被计算;并且不能缓存结果 ,因为参数是变化的。
这个主要的例子就是控制流切入点。
Spring的控制流切入点概念上和AspectJ的cflow 切入点一致,虽然没有其那么强大(当前没有办法指定一个切入点在另一个切入点后执行)。 一个控制流切入点匹配当前的调用栈。例如,连接点被 com.mycompany.web包或者 SomeCaller类中一个方法调用的时候,触发该切入点。控制流切入点的实现类是 org.springframework.aop.support.ControlFlowPointcut。
注意 | |
---|---|
控制流切入点是动态切入点中计算代价最高的。Java 1.4中, 它的运行开销是其他动态切入点的5倍。在Java 1.3中则超过10倍。 |
Spring提供非常实用的切入点的超类帮助你实现你自己的切入点。
因为静态切入点非常实用,你很可能子类化StaticMethodMatcherPointcut,如下所示。 这只需要实现一个抽象方法(虽然可以改写其它的方法来自定义行为)。
class TestStaticPointcut extends StaticMethodMatcherPointcut { public boolean matches(Method m, Class targetClass) { // return true if custom criteria match } }
当然也有动态切入点的超类。
Spring 1.0 RC2或以上版本,自定义切入点可以用于任何类型的通知。
现在让我们看看Spring AOP是如何处理通知的。
Spring的通知可以跨越多个被通知对象共享,或者每个被通知对象有自己的通知。这分别对应 per-class或per-instance 通知。
Per-class通知使用最为广泛。它适合于通用的通知,如事务adisor。它们不依赖被代理 的对象的状态,也不添加新的状态。它们仅仅作用于方法和方法的参数。
Per-instance通知适合于导入,来支持混入(mixin)。在这种情况下,通知添加状态到 被代理的对象。
可以在同一个AOP代理中混合使用共享和per-instance通知。
Spring提供几种现成的通知类型并可扩展提供任意的通知类型。让我们看看基本概念和 标准的通知类型。
Spring中最基本的通知类型是interception around advice .
Spring使用方法拦截器的around通知是和AOP联盟接口兼容的。实现around通知的 类需要实现接口MethodInterceptor:
public interface MethodInterceptor extends Interceptor { Object invoke(MethodInvocation invocation) throws Throwable; }
invoke()方法的MethodInvocation 参数暴露将被调用的方法、目标连接点、AOP代理和传递给被调用方法的参数。 invoke()方法应该返回调用的结果:连接点的返回值。
一个简单的MethodInterceptor实现看起来如下:
public class DebugInterceptor implements MethodInterceptor { public Object invoke(MethodInvocation invocation) throws Throwable { System.out.println("Before: invocation=[" + invocation + "]"); Object rval = invocation.proceed(); System.out.println("Invocation returned"); return rval; } }
注意MethodInvocation的proceed()方法的调用。 这个调用会应用到目标连接点的拦截器链中的每一个拦截器。大部分拦截器会调用这个方法,并返回它的返回值。但是, 一个MethodInterceptor,和任何around通知一样,可以返回不同的值或者抛出一个异常,而 不调用proceed方法。但是,没有好的原因你要这么做。
MethodInterceptor提供了和其他AOP联盟的兼容实现的交互能力。这一节下面 要讨论的其他的通知类型实现了AOP公共的概念,但是以Spring特定的方式。虽然使用特定 通知类型有很多优点,但如果你可能需要在其他的AOP框架中使用,请坚持使用MethodInterceptor around通知类型。注意目前切入点不能和其它框架交互操作,并且AOP联盟目前也没有定义切入 点接口。Before通知是一种简单的通知类型。 这个通知不需要一个MethodInvocation对象,因为它只在进入一个方法 前被调用。
Before通知的主要优点是它不需要调用proceed() 方法, 因此没有无意中忘掉继续执行拦截器链的可能性。
MethodBeforeAdvice接口如下所示。 (Spring的API设计允许成员变量的before通知,虽然一般的对象都可以应用成员变量拦截,但Spring 有可能永远不会实现它)。
public interface MethodBeforeAdvice extends BeforeAdvice { void before(Method m, Object[] args, Object target) throws Throwable; }
注意返回类型是void。 Before通知可以在连接点执行之前 插入自定义的行为,但是不能改变返回值。如果一个before通知抛出一个异常,这将中断拦截器 链的进一步执行。这个异常将沿着拦截器链后退着向上传播。如果这个异常是unchecked的,或者 出现在被调用的方法的签名中,它将会被直接传递给客户代码;否则,它将被AOP代理包装到一个unchecked 的异常里。
下面是Spring中一个before通知的例子,这个例子计数所有正常返回的方法:
public class CountingBeforeAdvice implements MethodBeforeAdvice { private int count; public void before(Method m, Object[] args, Object target) throws Throwable { ++count; } public int getCount() { return count; } }Before通知可以被用于任何类型的切入点。
如果连接点抛出异常,Throws通知 在连接点返回后被调用。Spring提供强类型的throws通知。注意这意味着 org.springframework.aop.ThrowsAdvice接口不包含任何方法: 它是一个标记接口,标识给定的对象实现了一个或多个强类型的throws通知方法。这些方法形式 如下:
afterThrowing([Method], [args], [target], subclassOfThrowable)
只有最后一个参数是必需的。 这样从一个参数到四个参数,依赖于通知是否对方法和方法 的参数感兴趣。下面是throws通知的例子。
如果抛出RemoteException异常(包括子类), 这个通知会被调用
public class RemoteThrowsAdvice implements ThrowsAdvice { public void afterThrowing(RemoteException ex) throws Throwable { // Do something with remote exception } }
如果抛出ServletException异常, 下面的通知会被调用。和上面的通知不一样,它声明了四个参数,所以它可以访问被调用的方法,方法的参数 和目标对象:
public static class ServletThrowsAdviceWithArguments implements ThrowsAdvice { public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) { // Do something will all arguments } }
最后一个例子演示了如何在一个类中使用两个方法来同时处理 RemoteException和ServletException 异常。任意个数的throws方法可以被组合在一个类中。
public static class CombinedThrowsAdvice implements ThrowsAdvice { public void afterThrowing(RemoteException ex) throws Throwable { // Do something with remote exception } public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) { // Do something will all arguments } }Throws通知可被用于任何类型的切入点。
Spring中的after returning通知必须实现 org.springframework.aop.AfterReturningAdvice 接口,如下所示:
public interface AfterReturningAdvice extends Advice { void afterReturning(Object returnValue, Method m, Object[] args, Object target) throws Throwable; }
After returning通知可以访问返回值(不能改变)、被调用的方法、方法的参数和 目标对象。
下面的after returning通知统计所有成功的没有抛出异常的方法调用:
public class CountingAfterReturningAdvice implements AfterReturningAdvice { private int count; public void afterReturning(Object returnValue, Method m, Object[] args, Object target) throws Throwable { ++count; } public int getCount() { return count; } }
这方法不改变执行路径。如果它抛出一个异常,这个异常而不是返回值将被沿着拦截器链 向上抛出。
After returning通知可被用于任何类型的切入点。Spring将introduction通知看作一种特殊类型的拦截通知。
Introduction需要实现IntroductionAdvisor, 和IntroductionInterceptor接口:
public interface IntroductionInterceptor extends MethodInterceptor { boolean implementsInterface(Class intf); }
继承自AOP联盟MethodInterceptor接口的 invoke()方法必须实现导入:也就是说,如果被调用的方法是在 导入的接口中,导入拦截器负责处理这个方法调用,它不能调用proceed() 方法。
Introduction通知不能被用于任何切入点,因为它只能作用于类层次上,而不是方法。 你可以只用InterceptionIntroductionAdvisor来实现导入通知,它有下面的方法:
public interface InterceptionIntroductionAdvisor extends InterceptionAdvisor { ClassFilter getClassFilter(); IntroductionInterceptor getIntroductionInterceptor(); Class[] getInterfaces(); }
这里没有MethodMatcher,因此也没有和导入通知关联的 切入点。只有类过滤是合乎逻辑的。
getInterfaces()方法返回advisor导入的接口。
让我们看看一个来自Spring测试套件中的简单例子。我们假设想要导入下面的接口到一个 或者多个对象中:
public interface Lockable { void lock(); void unlock(); boolean locked(); }
这个例子演示了一个mixin。我们想要能够 将被通知对象类型转换为Lockable,不管它们的类型,并且调用lock和unlock方法。如果我们调用 lock()方法,我们希望所有setter方法抛出LockedException异常。 这样我们能添加一个方面使的对象不可变,而它们不需要知道这一点:这是一个很好的AOP例 子。
首先,我们需要一个做大量转化的IntroductionInterceptor。 在这里,我们继承 org.springframework.aop.support.DelegatingIntroductionInterceptor 实用类。我们可以直接实现IntroductionInterceptor接口,但是大多数情况下 DelegatingIntroductionInterceptor是最合适的。
DelegatingIntroductionInterceptor的设计是将导入 委托到真正实现导入接口的接口,隐藏完成这些工作的拦截器。委托可以使用构造方法参数 设置到任何对象中;默认的委托就是自己(当无参数的构造方法被使用时)。这样在下面的 例子里,委托是DelegatingIntroductionInterceptor的子类 LockMixin。给定一个委托(默认是自身)的 DelegatingIntroductionInterceptor实例寻找被这个委托(而不 是IntroductionInterceptor)实现的所有接口,并支持它们中任何一个导入。子类如 LockMixin也可能调用suppressInterflace(Class intf) 方法隐藏不应暴露的接口。然而,不管IntroductionInterceptor 准备支持多少接口,IntroductionAdvisor将控制哪个接口将被实际 暴露。一个导入的接口将隐藏目标的同一个接口的所有实现。
这样,LockMixin继承DelegatingIntroductionInterceptor 并自己实现Lockable。父类自动选择支持导入的Lockable,所以我们不需要指定它。 用这种方法我们可以导入任意数量的接口。
注意locked实例变量的使用。这有效地添加额外的状态到目标 对象。
public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable { private boolean locked; public void lock() { this.locked = true; } public void unlock() { this.locked = false; } public boolean locked() { return this.locked; } public Object invoke(MethodInvocation invocation) throws Throwable { if (locked() && invocation.getMethod().getName().indexOf("set") == 0) throw new LockedException(); return super.invoke(invocation); } }
通常不要需要改写invoke()方法:实现 DelegatingIntroductionInterceptor就足够了,如果是导入的方法, DelegatingIntroductionInterceptor实现会调用委托方法, 否则继续沿着连接点处理。在现在的情况下,我们需要添加一个检查:在上锁 状态下不能调用setter方法。
所需的导入advisor是很简单的。只有保存一个独立的 LockMixin实例,并指定导入的接口,在这里就是 Lockable。一个稍微复杂一点例子可能需要一个导入拦截器(可以 定义成prototype)的引用:在这种情况下,LockMixin没有相关配置,所以我们简单地 使用new来创建它。
public class LockMixinAdvisor extends DefaultIntroductionAdvisor { public LockMixinAdvisor() { super(new LockMixin(), Lockable.class); } }
我们可以非常简单地使用这个advisor:它不需要任何配置。(但是,有一点 是必要的:就是不可能在没有IntroductionAdvisor 的情况下使用IntroductionInterceptor。) 和导入一样,通常 advisor必须是针对每个实例的,并且是有状态的。我们会有不同的的LockMixinAdvisor 每个被通知对象,会有不同的LockMixin。 advisor组成了被通知对象的状态的一部分。
和其他advisor一样,我们可以使用 Advised.addAdvisor() 方法以编程地方式使用这种advisor,或者在XML中配置(推荐这种方式)。 下面将讨论所有代理创建,包括“自动代理创建者”,选择代理创建以正确地处理导入和有状态的混入。
在Spring中,一个advisor就是一个aspect的完整的模块化表示。 一般地,一个advisor包括通知和切入点。
撇开导入这种特殊情况,任何advisor可被用于任何通知。 org.springframework.aop.support.DefaultPointcutAdvisor 是最通用的advisor类。例如,它可以和MethodInterceptor、 BeforeAdvice或者ThrowsAdvice一起使 用。
Spring中可以将advisor和通知混合在一个AOP代理中。例如,你可以在一个代理配置中 使用一个对around通知、throws通知和before通知的拦截:Spring将自动创建必要的拦截器链。
如果你在为你的业务对象使用Spring的IoC容器(例如ApplicationContext或者BeanFactory), 你应该会或者你愿意会使用Spring的aop FactoryBean(记住,factory bean引入了一个间接层, 它能创建不同类型的对象).
在spring中创建AOP proxy的基本途径是使用org.springframework.aop.framework.ProxyFactoryBean. 这样可以对pointcut和advice作精确控制。但是如果你不需要这种控制,那些简单的选择可能更适合你。
ProxyFactoryBean,和其他Spring的 FactoryBean实现一样,引入一个间接的层次。如果你 定义一个名字为foo的ProxyFactoryBean, 引用foo的对象所看到的不是ProxyFactoryBean 实例本身,而是由实现ProxyFactoryBean的类的 getObject()方法所创建的对象。这个方法将创建一个包装了目标对象 的AOP代理。
使用ProxyFactoryBean或者其他IoC可知的类来创建AOP代理 的最重要的优点之一是IoC可以管理通知和切入点。这是一个非常的强大的功能,能够实 现其他AOP框架很难实现的特定的方法。例如,一个通知本身可以引用应用对象(除了目标对象, 它在任何AOP框架中都可以引用应用对象),这完全得益于依赖注入所提供的可插入性。
类似于Spring提供的绝大部分FactoryBean实现一样, ProxyFactoryBean也是一个javabean,我们可以利用它的属性来:
指定你将要代理的目标
指定是否使用CGLIB
一些关键属性来自org.springframework.aop.framework.ProxyConfig :它是所有AOP代理工厂的父类。这些关键属性包括:
proxyTargetClass: 如果我们应该代理目标类, 而不是接口,这个属性的值为true。如果这是true,我们需要使用CGLIB。
optimize: 是否使用强优化来创建代理。不要使用 这个设置,除非你了解相关的AOP代理是如何处理优化的。目前这只对CGLIB代理有效;对JDK 动态代理无效(默认)。
frozen: 是否禁止通知的改变,一旦代理工厂已经配置。 默认是false。
exposeProxy: 当前代理是否要暴露在ThreadLocal中, 以便它可以被目标对象访问。(它可以通过MethodInvocation得到,不需要ThreadLocal)。 如果一个目标需要获得它的代理并且exposeProxy的值是ture,可以使用 AopContext.currentProxy()方法。
aopProxyFactory: 所使用的AopProxyFactory具体实现。 这个参数提供了一条途径来定义是否使用动态代理、CGLIB还是其他代理策略。默认实现将适当地选择动态 代理或CGLIB。一般不需要使用这个属性;它的意图是允许Spring 1.1使用另外新的代理类型。
其他ProxyFactoryBean特定的属性包括:
proxyInterfaces: 接口名称的字符串数组。如果这个 没有提供,CGLIB代理将被用于目标类。
interceptorNames: Advisor、interceptor或其他 被应用的通知名称的字符串数组。顺序是很重要的。这里的名称是当前工厂中bean的名称,包 括来自祖先工厂的bean的名称。
singleton: 工厂是否返回一个单独的对象,无论 getObject()被调用多少次。许多FactoryBean 的实现提供这个方法。默认值是true。如果你想要使用有状态的通知--例如,用于有状态的 mixin--将这个值设为false,使用prototype通知。
让我们来看一个简单的ProxyFactoryBean的实际例子。这个例子涉及到 :
一个将被代理的目标bean,在这个例子里,这个bean的被定义为"personTarget".
一个advisor和一个interceptor来提供advice.
一个AOP代理bean定义,该bean指定目标对象(这里是personTarget bean), 代理接口,和使用的advice.
<bean id="personTarget" class="com.mycompany.PersonImpl"> <property name="name"><value>Tony</value></property> <property name="age"><value>51</value></property> </bean> <bean id="myAdvisor" class="com.mycompany.MyAdvisor"> <property name="someProperty"><value>Custom string property value</value></property> </bean> <bean id="debugInterceptor" class="org.springframework.aop.interceptor.NopInterceptor"> </bean> <bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces"><value>com.mycompany.Person</value></property> <property name="target"><ref local="personTarget"/></property> <property name="interceptorNames"> <list> <value>myAdvisor</value> <value>debugInterceptor</value> </list> </property> </bean>
请注意:person bean的interceptorNames属性提供一个String列表, 列出的是该ProxyFactoryBean使用的,在当前bean工厂定义的interceptor或者advisor的 名字(advisor,interceptor,before,after returning,和throws advice 对象皆可)。 Advisor在该列表中的次序很重要。
你也许会对该列表为什么不采用bean的引用存有疑问。 原因就在于如果ProxyFactoryBean的singleton属性被设置为false, 那么bean工厂必须能返回多个独立的代理实例。 如果有任何一个advisor本身是prototype的,那么它就需要返回独立的实例, 也就是有必要从bean工厂获取advisor的不同实例,bean的引用在这里显然是不够的。上面定义的“person”bean定义可以作为Person接口的实现来使用,如下所示:
Person person = (Person) factory.getBean("person");
在同一个IoC的上下文中,其他的bean可以依赖于Person接口,就象依赖于一个普通的java对象一样。
<bean id="personUser" class="com.mycompany.PersonUser"> <property name="person"><ref local="person" /></property> </bean>
在这个例子里,PersonUser类暴露了一个类型为Person的属性。 只要是在用到该属性的地方,AOP代理都能透明的替代一个真实的Person实现。 但是,这个类可能是一个动态代理类。也就是有可能把它类型转换为一个Advised接口 (该接口在下面的章节中论述) 。
如果你需要代理的是类,而不是一个或多个接口,又该怎么办呢?
想象一下我们上面的例子,如果没有Person接口, 我们需要通知一个叫Person的类, 而且该类没有实现任何业务接口。在这种情况下,你可以配置Spring使用CGLIB代理, 而不是动态代理。你只要在上面的ProxyFactoryBean定义中把 它的proxyTargetClass属性改成true就行了。
只要你愿意,即使在有接口的情况下,你也可以强迫Spring使用CGLIB代理。
CGLIB代理是通过在运行期产生目标类的子类来进行工作的。 Spring可以配置这个生成的子类,来代理原始目标类的方法调用。这个子类是用 Decorator设计模式置入到advice中的。
CGLIB代理对于用户来说应该是透明的。然而,还有以下一些因素需要考虑:
Final方法不能被通知,因为不能被重写。
你需要在你的classpath中包括CGLIB的二进制代码,而动态代理对任何JDK都是可用的.
CGLIB和动态代理在性能上有微小的区别,对Spring 1.0来说,后者稍快。 另外,以后可能会有变化。在这种情况下性能不是决定性因素
通常,我们不需要ProxyFactoryBean的全部功能,因为我们常常只对一个方面感兴趣: 例如,事务管理。
当我们仅仅对一个特定的方面干兴趣时,我们可以使用许多便利的工厂来创建AOP代理。这些在其他 章节讨论,所以这里我们快速浏览一下它们。
用Spring提供的jPetStore的示例应用 演示了TransactionProxyFactoryBean的使用方式。
TransactionProxyFactoryBean是ProxyConfig的子类, 因此基本配置信息是和ProxyFactoryBean共享的。 (见上面ProxyConfig的属性列表。)
下面的代码来自于JPetStore application,演示了ProxyFactoryBean是如何工作的。 跟ProxyFactoryBean一样,存在一个目标bean的定义。 类的依赖关系定义在代理工厂bean定义中(petStore),而不是普通Java对象(petStoreTarget)。
TransactionProxyFactoryBean需要设置一个target属性, 还需要设置“transactionAttributes”, “transactionAttributes”用来指定需要事务化 处理的方法,还有要求的传播方式和其他设置:
<bean id="petStoreTarget" class="org.springframework.samples.jpetstore.domain.logic.PetStoreImpl"> <property name="accountDao"><ref bean="accountDao"/></property> <!-- Other dependencies omitted --> </bean> <bean id="petStore" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager"><ref bean="transactionManager"/></property> <property name="target"><ref local="petStoreTarget"/></property> <property name="transactionAttributes"> <props> <prop key="insert*">PROPAGATION_REQUIRED</prop> <prop key="update*">PROPAGATION_REQUIRED</prop> <prop key="*">PROPAGATION_REQUIRED,readOnly</prop> </props> </property> </bean>
TransactionProxyFactoryBean自动创建一个事务advisor, 该advisor包括一个基于事务属性的切入点。因此只有事务性的方法被通知。
TransactionProxyFactoryBean使用preInterceptors和 postInterceptors属性指定“pre”和“post”通知。它们将拦截器,通知和Advisor数组放置 在事务拦截器前后的拦截器链中。使用XML格式的bean定义中的<list>元素定义,就 象下面一样:
<property name="preInterceptors"> <list> <ref local="authorizationInterceptor"/> <ref local="notificationBeforeAdvice"/> </list> </property> <property name="postInterceptors"> <list> <ref local="myAdvisor"/> </list> </property>
这些属性可以加到上面的“petStore”的bean定义里。一个通用用法是将事务和声明式 安全组合在一起使用:一个和EJB提供的类似的方法。
因为使用前拦截器和后拦截器时,用的是真正的实例引用,而不象在 ProxyFactoryBean中用的bean的名字,因此它们只能用于共享实例的通知。 因此它们不能用在有状态的通知中:例如,在mixin中。这和TransactionProxyFactoryBean的要求是 一致的。如果你需要更复杂的,可以定制的AOP,你可以考虑使用普通的ProxyFactoryBean, 或者是自动代理生成器(参考下面)。
尤其是如果我们将Spring的AOP在许多情况下看成是EJB的替代品,我们会发现大多数通知是很普通的, 可以使用共享实例。声明式的事务管理和安全检查是一个典型的例子。TransactionProxyFactoryBean依赖于由它的transactionManager 属性指定的TransactionManager。 这种事务管理方式是可插拔的,基于JTA,JDBC或者其他事务管理策略皆可。 这与Spring的事务抽象层有关,而不在于AOP本身。我们将在下一章中讨论事务机制。
如果你只对声明性事务管理感兴趣,TransactionProxyFactoryBean是一个不错的解决办法, 并且比直接使用ProxyFactoryBean来得简单.使用Spring以编程的方式创建AOP代理也很简单。 这使得你不需要Spring的IoC就能够使用Spring的AOP。
下面的代码显示的用拦截器和advisor为目标对象创建代理。目标对象实现的接口将自动被代理:
ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl); factory.addInterceptor(myMethodInterceptor); factory.addAdvisor(myAdvisor); MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();
第一步是创建类型为org.springframework.aop.framework.ProxyFactory 的对象。你可以和上面的例子一样用目标对象创建,或者在另一个构造函数中指定要被代理的接口。
你可以添加拦截器或advisor,在整个ProxyFactory的生命周期内操作它们。如果你添加 IntroductionInterceptionAroundAdvisor,你可以使代理实现附加接口。
ProxyFactory(它是从AdvisedSupport继承而来)也提供了一些实用方法,使你可以添加 其它通知类型,比如before通知和throws通知。AdvisedSupport是ProxyFactory和ProxyFactoryBean 的父类。
将AOP代理的创建和IoC框架结合起来在大多数应用中都是最好的实现方式。我们推荐你和一般情况一样, 不要将AOP配置信息放在Java代码里。无论你怎么创建AOP代理,你都可以使用org.springframework.aop.framework.Advised 接口来操作它们。任何AOP代理无论实现其它什么接口,都可以类型转换为这个接口。这个接口包括下列方法:
void addInterceptor(Interceptor interceptor) throws AopConfigException; void addInterceptor(int pos, Interceptor interceptor) throws AopConfigException; void addAdvisor(Advisor advisor) throws AopConfigException; void addAdvisor(int pos, Advisor advisor) throws AopConfigException; int indexOf(Advisor advisor); boolean removeAdvisor(Advisor advisor) throws AopConfigException; void removeAdvisor(int index) throws AopConfigException; boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException; boolean isFrozen();
getAdvisors()方法为工厂中的每个advisor,拦截器或者其它通知类型返回一个Advisor。 如果你添加一个Advisor,使用当前索引返回的advisor就是你添加的对象。如果你添加拦截器或其它通知类型, Spring将当前对象和一个满足要求的切入点封装在一个advisor里。因此,如果你添加 MethodInterceptor, 使用当前索引返回的advisor是一个DefaultPointcutAdvisor,这个advisor返回 MethodInterceptor和满足所有类和方法的切入点。
addAdvisor()被用来添加Advisor。通常会是一个普通的 DefaultPointcutAdvisor,它可以和任何通知或切入点(除了引用)一起使用。
缺省情况下,在每次代理被创建的时候添加或删除advisor或拦截器。唯一的限制是不能添加或删除 引入advisor,因为工厂提供的已存在的代理不反映接口的变化。(你可以从工厂得到一个新的代理来避免这个问题)
是否建议在产品中修改业务对象的通知还值得怀疑,虽然毫无疑问存在合理的使用情况。但是, 在开发中这是非常有用的:例如,在测试中。我有时候发现以拦截器或其它通知的形式来添加测试代码非常有用, 这样就可以进入我想要测试的方法调用。(例如,通知可以进入为这个方法创建的事务中: 在为回滚事务作标记前,运行SQL检查数据库是否被正确更新。)根据你创建代理的方式,你通常可以设置frozen标记,这样Advised 的isFrozen()就返回true,任何添加或删除通知都将导致 AopConfigException。这种冻结被通知对象状态的方法在一些情况下是非常有用的: 例如,为了阻止调用代码删除一个安全拦截器。如果已知运行时修改通知不被允许,这还可以被Spring 1.1用来 作优化。
目前为止,我们已经讨论了使用ProxyFactoryBean或类似的工厂bean来显式创建AOP代理。
Spring也允许我们使用“autoproxy”的bean定义,它可以自动代理所选择的bean定义。这是建立在Spring的 “bean后处理器”机制上的,它能够在容器载入bean定义的时候修改任何bean定义。
在这个模型中,你可以在你的XML bean定义文件中建立特殊的bean定义,来配置自动代理机制。这允许你声明目标对象以使用自动代理功能: 你就可以不需要使用ProxyFactoryBean。
有两种方法来实现自动代理:
使用一个自动代理生成器,它引用当前上下文中的那些特殊bean
有一个特殊的自动代理创建的情况值得单独考虑:由源代码级元数据驱动的自动代理创建
org.springframework.aop.framework.autoproxy包提供了下列标准自动代理生成器。
BeanNameAutoProxyCreator为名字符合某个值或统配符的bean自动创建AOP代理。
<bean id="jdkBeanNameProxyCreator" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"> <property name="beanNames"><value>jdk*,onlyJdk</value></property> <property name="interceptorNames"> <list> <value>myInterceptor</value> </list> </property> </bean>
就和ProxyFactoryBean一样,有一个interceptorNames 属性,而不是一个拦截器列表,这个属性允许为prototype的advisor提供正确的行为。虽然名字叫 “拦截器”,但是也可以是advisor或任何通知类型。
就象一般的自动代理创建一样,使用BeanNameAutoProxyCreator的主要目的 是对多个对象使用相同的配置信息,并且减少配置的工作量。这在为多个对象使用声明式事务时是一个很流行的选择。
在上面的例子中,名字匹配的bean定义,如“jdkMyBean”和“onlyJdk”,是包含目标类的普通bean定义。 BeanNameAutoProxyCreator将自动创建AOP代理。相同的通知会被因用到所有匹配的bean。 注意,如果使用了advisor(而不是上面例子中的拦截器),切入点可能对不同的bean会不同。
DefaultAdvisorAutoProxyCreator是一个更通用,更强大的自动代理生成器。它将 自动应用于当前上下文的符合条件的advisor,而不需要在自动代理advisor的bean定义中包含特定的bean名字。 它有助于配置的一致性,并避免象BeanNameAutoProxyCreator一样重复配置。
使用这个机制包括:
指定一个DefaultAdvisorAutoProxyCreator的bean定义
在相同或相关上下文中指定任何数目的Advisor。注意这些必须是Advisor, 而不仅仅是拦截器或其它通知。这是很必要的,因为必须有一个切入点来检查每个通知是否符合候选bean定义。
DefaultAdvisorAutoProxyCreator会自动计算每个advisor包含的的切入点,看看 是否有什么通知应该被引用到每个业务对象(比如例子中的“businessObject1”和“businessObject2”)。
这意味着任何数目的advisor都可以自动应用到每个业务对象。如果advisor中没有任何切入点符合业务对象的 方法,这个对象就不会被代理。因为会为新的业务对象添加bean定义,如果必要,它们会自动被代理。
一般来说,自动代理可以保证调用者或依赖无法接触未被通知的对象。在这个ApplicationContext上 调用getBean("businessObject1")返回一个AOP代理,而不是目标业务对象。
<bean id="autoProxyCreator" class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"> </bean> <bean id="txAdvisor" autowire="constructor" class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor"> <property name="order"><value>1</value></property> </bean> <bean id="customAdvisor" class="com.mycompany.MyAdvisor"> </bean> <bean id="businessObject1" class="com.mycompany.BusinessObject1"> <!-- Properties omitted --> </bean> <bean id="businessObject2" class="com.mycompany.BusinessObject2"> </bean>
如果你想在几个业务对象上应用相同的通知,DefaultAdvisorAutoProxyCreator 就非常有用。一旦定义恰当,你可以简单地添加业务对象而不需要包括特定的代理配置。你也可以非常容易地 删除所附加的方面--例如,跟踪或性能监控的方面--可以尽可能减少配置修改。
DefaultAdvisorAutoProxyCreator支持过滤(使用命名规则以便只计算某一些 advisor,允许在一个工厂中使用多个,被不同配置的AdvisorAutoProxyCreator)和排序。Advisor可以实现 org.springframework.core.Ordered接口以保证正确的排序,如果排序确实需要。在 上面的例子中,TransactionAttributeSourceAdvisor有一个可配置的顺序值,缺损是不排序。
一种特别重要的自动代理类型是由元数据驱动的。这和.NET的ServicedComponents编程框架 非常类似。它没有象EJB那样使用XML部署描述,事务管理和其它企业级业务的配置都是定义在源代码级的属性上。
在这种情况下,你可以使用DefaultAdvisorAutoProxyCreator,以及可以读取元数据属性的 Advisor。元数据细节定义在候选advisor的切入点部分,而不是自动代理创建类本身。
这是DefaultAdvisorAutoProxyCreator的一种特殊情况,但是它本身而言是值得考虑的。 (可以读取元数据的代码处于advisor的切入点中,而不是AOP框架本身。)
jPetStore示例应用的/attributes目录演示了属性驱动的自动代理的使用。在这个例子中, 没有必要使用TransactionProxyFactoryBean。仅仅在业务对象上定义业务属性就足够了,因为 使用了可知元数据的切入点。bean定义在/WEB-INF/declarativeServices.xml中,包括下 面的代码。注意这是通用的,可以在jPetStore以外的地方使用:
<bean id="autoproxy" class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"> </bean> <bean id="transactionAttributeSource" class="org.springframework.transaction.interceptor.AttributesTransactionAttributeSource" autowire="constructor"> </bean> <bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor" autowire="byType"> </bean> <bean id="transactionAdvisor" class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor" autowire="constructor" > </bean> <bean id="attributes" class="org.springframework.metadata.commons.CommonsAttributes" />
DefaultAdvisorAutoProxyCreator bean定义--在这种情况下称作“advisor”,但是名字 无关紧要--会在当前的应用上午中选择所有符合的切入点。在这个例子中,类型为 TransactionAttributeSourceAdvisor的“transactionAdvisor”bean定义将会应用于包含事务属性 的类或方法。TransactionAttributeSourceAdvisor通过构造函数依赖于TransactionInterceptor。这个例子通过自动 装配来解析它。AttributesTransactionAttributeSource依赖于 org.springframework.metadata.Attributes接口的一个实现。在这段代码中,“attributes” bean使用Jakarta Commons Attributes API来获取属性信息。(应用代码必须使用Commons Attributes编译任务编译。)
这里定义的TransactionInterceptor依赖于一个 PlatformTransactionManager定义,它并没有被包括在这个通用的文件中(虽然应该是这样), 这是因为它是和应用的事务需求相关的(一般地,是想这个例子中的JTA,或者Hibernate,JDO 或JDBC):
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>如果你只要求声明式事务管理,使用这些通用的XML定义就可以使得Spring自动代理含有事务属性的所有类和方法。 你不需要直接和AOP打交道,并且编程模型和.NET的ServicedComponents非常相似。
这个机制具有可扩展性。它可以基于定制的属性来使用自动代理。你需要:
定义你的定制属性。
指定的Advisor包含必要的通知和由方法或类的定制属性所触发的切入点。你可以使用已经存在的通知,仅仅实 现用来选择定制属性的切入点。
这些advisor可能对每个被通知类都是唯一的(例如,maxin)。它们仅仅需要被定义成 prototype bean,而不是singleton bean。例如,Spring的测试套件中的LockMixin 引入拦截器可以和一个属性驱动切入点一起来定位一个maxin,就象这里演示的。我们使用JavaBean配置的 普通的DefaultPointcutAdvisor:
<bean id="lockMixin" class="org.springframework.aop.LockMixin" singleton="false" /> <bean id="lockableAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor" singleton="false" > <property name="pointcut"> <ref local="myAttributeAwarePointcut"/> </property> <property name="advice"> <ref local="lockMixin"/> </property> </bean> <bean id="anyBean" class="anyclass" ...
如果知道属性的切入点符合anyBean或者其它bean定义中的任何方法,这个maxin 将被应用。注意,lockMixin和lockableAdvisor定义都是 prototype的。myAttributeAwarePointcut切入点可以被定义成singleton,因为它不为 不同的被通知对象保存状态。
Spring提供了TargetSource的概念,由 org.springframework.aop.TargetSource接口定义。这个接口负责返回实现切入点的 “目标对象”。每次AOP代理处理方法调用时,目标实例都会用到TargetSource实现。
使用Spring AOP的开发者一般不需要直接使用TargetSources,但是这提供了一种强大的方法来支持池,热交换, 和其它复杂目标。例如,一个支持池的TargetSource可以在每次调用时返回不同的目标对象实例,使用池来管理实例。
如果你没有指定TargetSource,就使用缺省的实现,它封装了一个本地对象。每次调用会返回相同的目标对象 (和你期望的一样)。
让我们来看一下Spring提供的标准目标源,以及如何使用它们。
当使用定制目标源时,你的目标通常需要定义为prototype bean,而不是singleton bean。这使得 Spring在需要的时候创建一个新的目标实例。org.springframework.aop.target.HotSwappableTargetSource 允许切换一个AOP代理的目标,而调用者维持对它的引用。
修改目标源的目标会立即起作用。并且HotSwappableTargetSource是线程安全的。
你可以通过HotSwappableTargetSource的swap()方法 来改变目标,就象下面一样:
HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper"); Object oldTarget = swapper.swap(newTarget);
所需的XML定义如下:
<bean id="initialTarget" class="mycompany.OldTarget"> </bean> <bean id="swapper" class="org.springframework.aop.target.HotSwappableTargetSource"> <constructor-arg><ref local="initialTarget"/></constructor-arg> </bean> <bean id="swappable" class="org.springframework.aop.framework.ProxyFactoryBean" > <property name="targetSource"> <ref local="swapper"/> </property> </bean>
上面的swap()调用会修改swappable这个bean的目标。持有对这个bean应用的客户端 将不会知道这个变化,但会立刻转为使用新的目标对象。
虽然这个例子没有添加任何通知--使用TargetSource也没必要添加通知-- 当然任何TargetSource都可以和任何一种通知一起使用。
使用支持池的目标源提供了一种和无状态的session EJB类似的编程模式,在无状态的session EJB中,维护了 一个相同实例的池,提供从池中获取可用对象的方法。
Spring的池和SLSB的池之间的重要区别在于Spring的池可以被应用到任何普通Java对象。就象Spring的通用 的做法,这个业务也可以以非侵入的方式被应用。
Spring直接支持Jakarta Commons Pool 1.1,它是一种非常高效的池实现。使用这个功能,你需要在你的应用的 classpath中添加commons-pool的Jar文件。也可以直接继承 org.springframework.aop.target.AbstractPoolingTargetSource来支持其它池API。
下面是一个配置的例子:
<bean id="businessObjectTarget" class="com.mycompany.MyBusinessObject" singleton="false"> ... properties omitted </bean> <bean id="poolTargetSource" class="org.springframework.aop.target.CommonsPoolTargetSource"> <property name="targetBeanName"><value>businessObject</value></property> <property name="maxSize"><value>25</value></property> </bean> <bean id="businessObject" class="org.springframework.aop.framework.ProxyFactoryBean" > <property name="targetSource"><ref local="poolTargetSource"/></property> <property name="interceptorNames"><value>myInterceptor</value></property> </bean>
注意例子中的目标对象“businessObjectTarget”必须是prototype。这样在 PoolingTargetSource的实现在扩大池容量的时候可以创建目标的新实例。关于这些属性的 信息可以参考AbstractPoolingTargetSource和子类的Javadoc。maxSize是最基本的属性, 被保证总是存在。
在这种情况下,名字为“myInterceptor”的拦截器需要定义在同一个IoC上下文中。但是,并不一定需要 指定拦截器也用池。如果你仅需要池,并且没有其它通知,可以根本不设置属性interceptorNames。
也可以配置Spring以便可以将任何池化的对象转换类型为 org.springframework.aop.target.PoolingConfig接口。通过这个接口的一个引入,可以得到 配置信息和池的当前大小。你需要这样定义一个advisor:
<bean id="poolConfigAdvisor" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="target"><ref local="poolTargetSource" /></property> <property name="targetMethod"><value>getPoolingConfigMixin</value></property> </bean>
通过调用AbstractPoolingTargetSource类上的方法,可以得到这个advisor, 因此使用MethodInvokingFactoryBean。这个advisor的名字(“poolConfigAdvisor”)必须在暴露池化对象的 This advisor is obtained by calling a convenience method on the ProxyFactoryBean中的拦截器名字列表中。
这个类型转换就象下面:
PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject"); System.out.println("Max pool size is " + conf.getMaxSize());池化无状态业务对象并不是总是必要的。我们不认为这是缺省选择,因为大多数无状态对象自然就是线程 安全的,如果资源被缓存,实例池化会有问题。
简单的池也可以使用自动代理。任何自动代理生成器都可以设置TargetSources。
设置“prototype”目标源和支持池的目标源类似。在每次方法调用的时候都会创建一个新的目标实例。 虽然在现代JVM中创建对象的代价不是很高,但是装配新对象的代价可能更高(为了maz满足它的IoC依赖关系)。 因此没有好的理由不应该使用这个方法。
为了这么做,你可以修改上面的的poolTargetSource定义,就向下面一样。 (为了清晰起见,我修改了名字。)
<bean id="prototypeTargetSource" class="org.springframework.aop.target.PrototypeTargetSource"> <property name="targetBeanName"><value>businessObject</value></property> </bean>
只有一个属性:目标bean的名字。在TargetSource实现中使用继承是为了保证命名的一致性。就象支持池的 目标源一样,目标bean必须是一个prototype的bean定义。
Spring AOP设计能够很容易地扩展。虽然拦截实现的策略目前只在内部使用,但还是有可能支持拦截around通知, before通知,throws通知和after returning通知以外的任何通知类型。
org.springframework.aop.framework.adapter 包是一个支持添加新的定制通知类型而不修改核心框架的SPI(译:可能是API)包。定制通知类型的唯一限制是它必须实现 org.aopalliance.aop.Advice标记接口。
更多信息请参考org.springframework.aop.framework.adapter包的Javadoc。
对于AOP的介绍,我推荐Ramnivas Laddad (Manning, 2003)写的AspectJ in Action。
进一步的Spring AOP的例子请参考Spring的示例应用:
JPetStore的缺省配置演示了使用TransactionProxyFactoryBean来定义声明式事务管理。
JPetStore的/attributes目录演示了属性驱动的声明式事务管理。
如果你对Spring AOP更多高级功能感兴趣,可以看一下测试套件。测试覆盖率超过90%,并且演示了本文档没有提到 的许多高级功能。
Spring AOP,就象Spring的其它部分,是开发非常活跃的部分。核心API已经稳定了。象Spring的其它部分一样, AOP框架是非常模块化的,在保留基础设计的同时提供扩展。在Spring 1.1到1.2阶段有很多地方可能会有所提高,但是这 些地方也保留了向后兼容性。它们是:
性能的提高:AOP代理的创建由工厂通过策略接口处理。因此我们能够支持额外的AOP 代理类型而不影响用户代码或核心实现。对于Spring 1.1,我们正在检查AOP代理实现的所有字节码,万一不需要 运行时通知改变。这应该大大减少AOP框架的额外操作。但是注意,AOP框架的额外操作不是在普通使用中需要考虑 的内容。
更具表达力的切入点:Spring目前提供了一个具有表达力的切入点接口,但是我们 添加更多的切入点实现。我们正在考虑提供一个简单但具有强大表达式语言的实现。如果你希望贡献一个有用的 切入点实现,我们将非常欢迎。
引入方面这个高层概念,它包含多个advisor。
(六)