Spring 2.0宝典读书笔记

来源:百度文库 编辑:神马文学网 时间:2024/04/28 06:12:00
Spring的功能和特点大致上被组织成如下七个部分:
q      核心机制
q      Context容器
q      Web支持
q      MVC框架
q      DAO支持
q      ORM支持
q      面向切面编程支持
核心机制以BeanFactory为基础,管理bean与bean之间的依赖。ApplicationContext容器是BeanFactory功能加强的子接口。Web支持提供Web应用开发的支持。MVC框架则是针对Web应用的MVC思想实现;DAO支持包括一致的异常处理和编程方式;ORM支持则用于与流行的ORM框架的整合;面向切面编程提供与AOP联盟兼容的编程实现。
1.3.1  Spring的核心和Context
Spring以bean的方式组织、管理Java应用中各组件,组件之间的依赖关系松耦合运行良好。这些都得益于Spring的核心机制:依赖注入。Spring使用BeanFactory作为应用中负责生产和管理各组件的工厂,同时也是组件运行的容器。BeanFactory根据配置文件确定容器中bean的实现,管理bean之间的依赖关系。
bean之间的依赖关系由BeanFactory管理,bean的具体实例化过程也由BeanFactory来管理。将bean对bean实现类的依赖解藕,变成对接口的依赖。程序从面向具体类的编程,转向编程面向接口编程。极大地降低应用中组件的耦合。
ApplicationContext是BeanFactory的加强,ApplicationContext接口提供在J2EE应用中的大量增强功能,比如随Web应用启动的自动创建,程序国际化等。
1.3.2 Spring的Web和MVC
Spring的Web框架围绕分发器(DispatcherServlet)设计,DispatcherServlet将请求分发到不同的处理器。Spring的MVC框架提供了清晰的角色划分:控制器,验证器,命令对象,表单对象和模型对象,分发器,处理器映射和视图解析器。Spring支持多种表现层技术:Velocity,XSLT等等,甚至可以直接输出pdf电子文档,或excel文档。
1.3.3 Spring的面向切面编程
面向切面编程(AOP)完善Spring的依赖注入(DI)。面向切面编程在Spring中主要表现为如下几个方面:
q      面向切面编程提供声明式事务管理。
q      Spring支持用户自定义切面。
Spring也能与AspectJ整合。Spring允许通过依赖注入配置AspectJ的切面。
1.3.4 Spring的持久化支持
Spring对各种持久化技术提供一致的编程方式,不管是最直接的JDBC,还是各种流行的ORM框架如:Hibernate,iBATIS,JDO等,Spring都提供一致的异常继承体系。
Spring使用模板封装持久化访问的通用步骤。来自底层数据库的异常都是难以恢复的,因此,Spring将数据库访问的checked异常转换为运行时异常,避免烦琐的try…catch块。
Spring实现了两种基本设计模式:
q      工厂模式
q      单态模式
Spring容器是实例化和管理全部bean的工厂,工厂模式可将Java对象的调用者从被调用者的实现逻辑中分离出来,调用者只关心被调用者必须满足的某种规则(接口),而不必关心实例的具体实现过程,具体的实现由bean工厂完成。
Spring默认将所有的bean设置成单态模式,即:对所有相同id的bean的请求,都将返回同一个共享实例。单态模式可大大降低Java对象创建和销毁时的系统开销。使用Spring将bean设成单态行为,则无须自己完成单态模式。
1.4.1 单态模式的回顾
单态模式限制了类实例的创建,采用这种模式设计的类,可以保证仅有一个实例,并提供访问该实例的全局访问点。J2EE应用的大量组件,都需要保证一个类只有一个实例。比如数据库引擎访问点只能有一个。更多的时候,为了提高性能,程序应尽量减少Java对象的创建和销毁时开销。使用单态模式可避免Java类的频繁实例化,让相同类的全部实例共享同一内存区。
为了防止单态模式的类被多次实例化,应将类的构造器设成私有的,这样,保证只能通过静态方法获得类实例。而该静态方法则保证每次返回的实例都是同一个,这就需将该类的实例设置成类属性,该属性需要被静态方法访问,因此该属性应设成静态属性。下面给出单态模式的示例代码:
//单态模式测试类
public class SingletonTest
{
//该类的一个普通属性。
int value ;
//使用静态属性类保存该类的一个实例。
private static SingletonTest instance;
//构造器私有化,避免该类被多次实例。
private SingletonTest()
{
System.out.println(“正在执行构造器…”);
}
//提供静态方法来返回该类的实例。
public static SingletonTest getInstance()
{
//实例化类实例前,先检查该类的实例是否存在
if (instance == null)
{
//如果不存在,则新建一个实例。
instance = new SingletonTest();
}
//返回该类的成员变量:该类的实例。
return instance;
}
//以下提供对普通属性value的setter和getter方法
public int getValue()
{
return value;
}
public void setValue(int values)
{
this.value = value;
}
public static void main(String[] args)
{
SingletonTest  t1 = SingletonTest .getInstance();
SingletonTest  t2 = SingletonTest .getInstance();
t2.setValue(9);
System.out.println(t1 == t2);
}
}
根据程序最后的打印结果,可以看出类的两个实例完全相同,这证明:单态模式的类的全部实例是同一共享实例。程序里虽然获得了类的两个实例,但实际上只执行一次构造器,因为对于单态模式的类,不管有多少次的创建实例请求,都只执行一次构造器。
1.4.2 工厂模式的回顾
工厂模式根据调用数据返回某个类的一个实例,此类可能是多个类的某一个类。通常,这些类满足共同的规则(接口)或父类。调用者只关心工厂生产的实例是否满足某种规范,即实现了某个接口;是否可供自己正常调用(调用者仅仅使用)。该模式提供各对象之间清晰的角色划分,降低程序的耦合。
接口产生的全部实例通常实现相同接口,接口里定义全部实例共同拥有的方法,这些方法在不同的实现类中实现方式不同。程序调用者无须关心方法的具体实现,从而降低系统异构的代价。下面是工厂模式的示例代码:
//Person接口定义
public interface Person
{
/**
* @param name 对name打招呼
* @return 打招呼的字符串
*/
public String sayHello(String name);
/**
* @param name 对name告别
* @return 告别的字符串
*/
public String sayGoodBye(String name);
}
该接口定义Person的规范,该接口必须拥有两个方法:能打招呼、能告别。规范要求实现该接口的类必须具有这两个方法:
//American类实现Person接口
public class American implements Person
{
/**
* @param name 对name打招呼
* @return 打招呼的字符串
*/
public String sayHello(String name)
{
return name + ",Hello";
}
/**
* @param name 对name告别
* @return 告别的字符串
*/
public String sayGoodBye(String name)
{
return name + ",Good Bye";
}
}
下面是实现Person接口的另一个实现类:Chinese
public class Chinese implements Person
{
/**
* @param name 对name打招呼
* @return 打招呼的字符串
*/
public String sayHello(String name)
{
return name + ",您好";
}
/**
* @param name 对name告别
* @return 告别的字符串
*/
public String sayGoodBye(String name)
{
return name + ",下次再见";
}
}
然后看Person工厂的代码:
public class PersonFactory
{
/**
* 获得Person实例的实例工厂方法
* @ param ethnic 调用该实例工厂方法传入的参数
* @ return返回Person实例
*/
public Person getPerson(String ethnic)
{
//根据参数返回Person接口的实例。
if (ethnic.equalsIgnoreCase("chin"))
{
return new Chinese();
}
else
{
return new American();
}
}
}
最简单的工厂模式的框架基本如上所示。
主程序部分仅仅需要与工厂耦合,而无须与具体的实现类耦合在一起。下面是主程序部分:
public class FactroyTest
{
public static void main(String[] args)
{
//创建PersonFactory的实例,获得工厂实例
PersonFactory pf = new PersonFactory();
//定义接口Person的实例,面向接口编程
Person p = null;
//使用工厂获得Person的实例
p = pf.getPerson("chin");
//下面调用Person接口的方法
System.out.println(p.sayHello("wawa"));
System.out.println(p.sayGoodBye("wawa"));
//使用工厂获得Person的另一个实例
p = pf.getPerson("ame");
//再次调用Person接口的方法
System.out.println(p.sayHello("wawa"));
System.out.println(p.sayGoodBye("wawa"));
}
}
主程序从Person接口的具体类中解耦出来,而且,程序调用者无须关心Person的实例化过程,角色划分清晰。主程序仅仅与工厂服务定位结合在一起:获得工厂的引用,程序将可获得所有工厂能产生的实例。具体类的变化,重要接口不发生任何改变,调用者程序代码部分几乎无须发生任何改动。
下面看Spring对这两种模式的实现。
1.4.3 Spring对单态与工厂模式的实现
无须修改程序的接口和实现类。Spring提供工厂模式的实现,因此,对于PersonFactory工厂类,此处不再需要。Spring使用配置文件管理所有的bean,该bean就是Spring工厂能产生的全部实例。下面是关于两个实例的配置文件:


"http://www.springframework.org/dtd/spring-beans.dtd">





主程序部分如下:
public class SpringTest
{
public static void main(String[] args)
{
//实例化Spring容器
ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml");
//定义Person接口的实例
Person p = null;
//通过Spring上下文获得chinese实例
p = (Person)ctx.getBean("chinese");
//执行chinese实例的方法
System.out.println(p.sayHello("wawa"));
System.out.println(p.sayGoodBye("wawa"));
//通过Spring上下文获得american实例
p = (Person)ctx.getBean("american");
//执行american实例的方法
System.out.println(p.sayHello("wawa"));
System.out.println(p.sayGoodBye("wawa"));
}
}
使用Spring至少有一个好处:即使没有工厂类PersonFactory,程序一样可以使用工厂模式。所有工厂模式的功能,Spring完全可以提供。下面对主程序部分做出简单的修改:
public class SpringTest
{
public static void main(String[] args)
{
//实例化Spring容器
ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml");
//定义Person接口的实例p1
Person p1 = null;
//通过Spring上下文获得chinese实例
p1 = (Person)ctx.getBean("chinese");
//定义Person接口的实例p1
Person p2 = null;
p2 = (Person)ctx.getBean("chinese");
System.out.println(p1 == p2);
}
}
程序执行结果是:
true
表明:Spring对接受容器管理的全部bean,默认采用单态模式管理。除非必要,笔者建议不要随便更改bean的行为方式:性能上,单态的bean比非单态的bean更优秀。
仔细检查上面的代码,发现如下特点:
q      除测试用主程序部分,代码并未出现Spring特定的类和接口。
q      调用者代码,也就是测试用主程序部分,仅仅面向Person接口编程。而无须知道实现类的具体名称。同时,可以通过修改配置文件来切换底层的具体实现类。
q      工厂通常无须多个实例,因此,工厂应该采用单态模式设计。Spring的上下文,也就是产生bean实例的工厂,已被设计成单态的。
Spring实现的工厂模式,不仅提供了创建bean的功能,还提供对bean生命周期的管理。最重要的是:还可管理bean与bean之间的依赖关系,以及bean的属性值。
Spring能有效地组织J2EE应用各层的对象。不管是控制层的Action对象,还是业务层的Service对象,还是持久层的DAO对象,都可在Spring的管理下有机地协调、运行。Spring将各层的对象以松耦合的方式组织在一起,Action对象无须关心Service对象的具体实现,Service对象无须关心持久层对象的具体实现,各层对象的调用完全面向接口。当系统需要重构时,代码的改写量将大大减少。
上面所说的一切都得宜于Spring的核心机制,依赖注入。依赖注入让bean与bean之间以配置文件组织在一起,而不是以硬编码的方式耦合在一起。1.5.1 理解依赖注入
依赖注入(Dependency Injection)和控制反转(Inversion of Control)是同一个概念。具体含义是:当某个角色(可能是一个Java实例,调用者)需要另一个角色(另一个Java实例,被调用者)的协助时,在传统的程序设计过程中,通常由调用者来创建被调用者的实例。但在Spring里,创建被调用者的工作不再由调用者来完成,因此称为控制反转;创建被调用者实例的工作通常由Spring容器来完成,然后注入调用者,因此也称为依赖注入。
不管是依赖注入,还是控制反转,都说明Spring采用动态、灵活的方式来管理各种对象。对象与对象之间的具体实现互相透明。在理解依赖注入之前,看如下这个问题在各种社会形态里如何解决:一个人(Java实例,调用者)需要一把斧子(Java实例,被调用者)。
(1)原始社会里,几乎没有社会分工。需要斧子的人(调用者)只能自己去磨一把斧子(被调用者)。对应的情形为:Java程序里的调用者自己创建被调用者。
(2)进入工业社会,工厂出现。斧子不再由普通人完成,而在工厂里被生产出来,此时需要斧子的人(调用者)找到工厂,购买斧子,无须关心斧子的制造过程。对应Java程序的简单工厂的设计模式。
(3)进入“按需分配”社会,需要斧子的人不需要找到工厂,坐在家里发出一个简单指令:需要斧子。斧子就自然出现在他面前。对应Spring的依赖注入。
第一种情况下,Java实例的调用者创建被调用的Java实例,必然要求被调用的Java类出现在调用者的代码里。无法实现二者之间的松耦合。
第二种情况下,调用者无须关心被调用者具体实现过程,只需要找到符合某种标准(接口)的实例,即可使用。此时调用的代码面向接口编程,可以让调用者和被调用者解耦,这也是工厂模式大量使用的原因。但调用者需要自己定位工厂,调用者与特定工厂耦合在一起。
第三种情况下,调用者无须自己定位工厂,程序运行到需要被调用者时,系统自动提供被调用者实例。事实上,调用者和被调用者都处于Spring的管理下,二者之间的依赖关系由Spring提供。
所谓依赖注入,是指程序运行过程中,如果需要调用另一个对象协助时,无须在代码中创建被调用者,而是依赖于外部的注入。Spring的依赖注入对调用者和被调用者几乎没有任何要求,完全支持对POJO之间依赖关系的管理。依赖注入通常有两种:
q      设值注入。
q      构造注入。
1.5.2 设值注入
设值注入是指通过setter方法传入被调用者的实例。这种注入方式简单、直观,因而在Spring的依赖注入里大量使用。看下面代码,是Person的接口
//定义Person接口
public interface Person
{
//Person接口里定义一个使用斧子的方法
public void useAxe();
}
然后是Axe的接口
//定义Axe接口
public interface Axe
{
//Axe接口里有个砍的方法
public void chop();
}
Person的实现类
//Chinese实现Person接口
public class Chinese implements Person
{
//面向Axe接口编程,而不是具体的实现类
private Axe axe;
//默认的构造器
public Chinese()
{
}
//设值注入所需的setter方法
public void setAxe(Axe axe)
{
this.axe = axe;
}
//实现Person接口的useAxe方法
public void useAxe()
{
System.out.println(axe.chop());
}
}
Axe的第一个实现类
//Axe的第一个实现类 StoneAxe
public class StoneAxe implements Axe
{
//默认构造器
public StoneAxe()
{
}
//实现Axe接口的chop方法
public String chop()
{
return "石斧砍柴好慢";
}
}
下面采用Spring的配置文件将Person实例和Axe实例组织在一起。配置文件如下所示:



"http://www.springframework.org/dtd/spring-beans.dtd">














从配置文件中,可以看到Spring管理bean的灵巧性。bean与bean之间的依赖关系放在配置文件里组织,而不是写在代码里。通过配置文件的指定,Spring能精确地为每个bean注入属性。因此,配置文件里的bean的class元素,不能仅仅是接口,而必须是真正的实现类。
Spring会自动接管每个bean定义里的property元素定义。Spring会在执行无参数的构造器后、创建默认的bean实例后,调用对应的setter方法为程序注入属性值。property定义的属性值将不再由该bean来主动创建、管理,而改为被动接收Spring的注入。
每个bean的id属性是该bean的惟一标识,程序通过id属性访问bean,bean与bean的依赖关系也通过id属性完成。
下面看主程序部分:
public class BeanTest
{
//主方法,程序的入口
public static void main(String[] args)throws Exception
{
//因为是独立的应用程序,显式地实例化Spring的上下文。
ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml");
//通过Person bean的id来获取bean实例,面向接口编程,因此
//此处强制类型转换为接口类型
Person p = (Person)ctx.getBean("chinese");
//直接执行Person的userAxe()方法。
p.useAxe();
}
}
程序的执行结果如下:
石斧砍柴好慢
主程序调用Person的useAxe()方法时,该方法的方法体内需要使用Axe的实例,但程序里没有任何地方将特定的Person实例和Axe实例耦合在一起。或者说,程序里没有为Person实例传入Axe的实例,Axe实例由Spring在运行期间动态注入。
Person实例不仅不需要了解Axe实例的具体实现,甚至无须了解Axe的创建过程。程序在运行到需要Axe实例的时候,Spring创建了Axe实例,然后注入给需要Axe实例的调用者。Person实例运行到需要Axe实例的地方,自然就产生了Axe实例,用来供Person实例使用。
调用者不仅无须关心被调用者的实现过程,连工厂定位都可以省略(真是按需分配啊!)。下面也给出使用Ant编译和运行该应用的简单脚本:















deprecation="false" optimize="false" failonerror="true">















如果需要改写Axe的实现类。或者说,提供另一个实现类给Person实例使用。Person接口、Chinese类都无须改变。只需提供另一个Axe的实现,然后对配置文件进行简单的修改即可。
Axe的另一个实现如下:
//Axe的另一个实现类 SteelAxe
public class SteelAxe implements Axe
{
//默认构造器
public SteelAxe()
{
}
//实现Axe接口的chop方法
public String chop()
{
return "钢斧砍柴真快";
}
}
然后,修改原来的Spring配置文件,在其中增加如下一行:


该行重新定义了一个Axe的实现:SteelAxe。然后修改chinese bean的配置,将原来传入stoneAxe的地方改为传入steelAxe。也就是将

改成

此时再次执行程序,将得到如下结果:
钢斧砍柴真快
Person与Axe之间没有任何代码耦合关系,bean与bean之间的依赖关系由Spring管理。采用setter方法为目标bean注入属性的方式,称为设值注入。
业务对象的更换变得相当简单,对象与对象之间的依赖关系从代码里分离出来,通过配置文件动态管理。
1.5.3 构造注入
所谓构造注入,指通过构造函数来完成依赖关系的设定,而不是通过setter方法。对前面代码Chinese类做简单的修改,修改后的代码如下:
//Chinese实现Person接口
public class Chinese implements Person
{
//面向Axe接口编程,而不是具体的实现类
private Axe axe;
//默认的构造器
public Chinese()
{
}
//构造注入所需的带参数的构造器
public Chinse(Axe axe)
{
this.axe = axe;
}
//实现Person接口的useAxe方法
public void useAxe()
{
System.out.println(axe.chop());
}
}
此时无须Chinese类里的setAxe方法,构造Person实例时,Spring为Person实例注入所依赖的Axe实例。构造注入的配置文件也需做简单的修改,修改后的配置文件如下:



"http://www.springframework.org/dtd/spring-beans.dtd">










执行效果与使用steelAxe设值注入时的执行效果完全一样。区别在于:创建Person实例中Axe属性的时机不同——设值注入是现创建一个默认的bean实例,然后调用对应的构造方法注入依赖关系。而构造注入则在创建bean实例时,已经完成了依赖关系的注入。
1.5.4 两种注入方式的对比
设值注入和构造注入,都是Spring支持的依赖注入模式。也是目前主流的依赖注入模式。两种注入模式各有优点:
1.5.4.1 设值注入的优点
q      与传统的JavaBean的写法更相似,程序开发人员更容易了解、接受。通过setter方法设定依赖关系显得更加直观、自然。
q      对于复杂的依赖关系,如果采用构造注入,会导致构造器过于臃肿,难以阅读。Spring在创建bean实例时,需要同时实例化其依赖的全部实例,因而导致性能下降。而使用设值注入,则能避免这些问题。
q      尤其是某些属性可选的情况下,多参数的构造器更加笨重。
1.5.4.2 构造注入的优点
q      可以在构造器中决定依赖关系的注入顺序。优先依赖的优先注入。例如,组件中其他依赖关系的注入,常常需要依赖于Datasource的注入。采用构造注入,可以在代码中清晰地决定注入顺序。
q      对于依赖关系无须变化的bean,构造注入更有用处。因为没有setter方法,所有的依赖关系全部在构造器内设定。因此,无须担心后续的代码对依赖关系产生破坏。
q      依赖关系只能在构造器中设定,则只有组件的创建者才能改变组件的依赖关系。对组件的调用者而言,组件内部的依赖关系完全透明,更符合高内聚的原则。
建议采用以设值注入为主,构造注入为辅的注入策略。对于依赖关系无须变化的注入,尽量采用构造注入;而其他的依赖关系的注入,则考虑采用设值注入。
本章简短的介绍了Spring的起源和背景。分别讲述了利用Eclipse和Ant开发Spring应用的步骤。同时也对Spring体系作了一个简单预览,以便读者对Spring架构形成总体把握。本章还详细介绍了两种基本设计模式、两种设计模式在J2EE应用中的用法,以及Spring对两种模式的实现。
最后形象地介绍了依赖注入的概念,详细地讲解了Spring对两种依赖注入的支持和用法。同时对两种依赖注入进行简单的对比。请读者务必理解Spring的核心机制:依赖注入。此外,设值注入、构造注入应能理解各自的区别,并可以正确应用。