Web开发教程3-理解Spring设计模式
程序员的春天来了!在这章中,您将开始接触Spring,学习Spring基础知识。并将看到Spring在实现OCP原则上所做的努力,接触到为实现OCP原则所产生的两个设计模式:DI依赖及IoC控制反转。此外,在最后,您还将学习到Spring在使用时应注意的问题。
什么是Spring以及使用它的意义
Spring框架十分受欢迎,并且发展迅速。其成功原因很大程度上源于它的设计思想。Spring框架的核心思想是 IoC及DI 。用句简单的话来解释,就是让程序的各个组件之间独立开来,每个零部件只要接口规格一致,就可以自由更换。这就使得应用了Spring框架的项目变得非常灵活。当然,如果仅仅只有这点优势,Spring也不会取得如此大的成功。除了使用了很好的设计思想,Spring还提供一套非常完整且实用的工具箱,并且这套工具箱秉承一种哲学:别人做得好的,直接拿来用,并在上层提供更方便的功能;别人做得不好的,自已做,并做到最好。因为使用了IoC及DI模式,Spring框架的各个部件都可以自由更换,因此它可以随时调整,保持每一个工具都十分优秀。
Spring既然被称为框架(Framework),就说明它包含了您在开发过程中要用到了各种工具,使Java开发人员有一整套可使用的工具包,不必再从轮子造起。在下图中展示了Spring提供的工具箱:
除了IoC核心以外,Spring在上层提供的工具涉及到J2EE领域的方方面面。在本文的后续部分,会陆续介绍DAO、ORM、JEE、WEB、AOP这几块的使用。
什么是IoC及DI
我们已经学习了OCP法则,那么如何才能保证您的代码遵守了OCP法则呢? Martin Fowler 提出了一种设计模式,叫做Dependency Injection,简称DI,中文叫做依赖注入。而通过使用这种模式实现了IoC,即Inversion Of Control,中文翻译成控制反转。通过这种模式,可以使项目符合OCP的设计原则。而Spring最核心的理念就是IoC及DI,并在这个设计思想上提供了一大套工具箱。名词比较抽象,下面直接通过实例来看一下其具体含义。
Spring的基本使用方式
现在制作一个用户听收音机的程序,程序中有两个类:用户User,收音机Radio。主程序的逻辑为用户使用收音机:
用户有姓名及收音机两个属性。收音机有一个use()方法。下面来看具体实现:
package demo; public class Radio { public String use() { return "RADIO"; } }
下面是用户类的实现1:
package demo; public class User { private String username; private Radio radio; public Radio getRadio() { return radio; } public void setRadio(Radio radio) { this.radio = radio; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } }
1 在讲解OCP法则时,讲过类的属性要封装,因此User类的username及radio属性要声明为private,而使用getter及setter方法去操作它们。 用户类中有两个属性:姓名,收音机。现在开始实现业务逻辑,即用户使用收音机:
package demo; public class UserPlayRadio { public static void main(String[] args) { User user = new User(); Radio radio = new Radio(); user.setUsername("Peter"); user.setRadio(radio); System.out.println(user.getUsername() + " is using " + user.getRadio().use()); } }
首先创建一个用户,然后创建一个收音机,并把这个收音机交给用户。最后,用户取得收音机,并播放它。系统最终返回结果: Peter is using RADIO 这个设计似乎没什么问题,可是没过多久,客户跑过来找您,说这个程序需要添加一个功能:User除了使用收音机以外,还要使用电视。这下麻烦了,这个程序中用户包含的属性固定是收音机。如果还要添加看电视机的功能,除去添加电视机类别Tv以外,还必须修改User类,在其中添加电视机的属性。这样太不合理了,用户应该有能力去使用各种各样的电器。可是到了代码里,多了一个Tv,User类也必须随之更改。在这种情况下,可以使用接口来优化设计,将User对Radio及可能的Tv类别的依赖关系调整过来。 首先定义一个接口类别:电器Appliance。这个接口规定一个功能:use(),即所有的电器都可以被使用: package demo; public interface Appliance { public String use(); } 接下来是改造User类别,将Radio属性去掉,代之以接口Appliance:
package demo; public class UserV2 { private String username; private Appliance appliance; public Appliance getAppliance() { return appliance; } public void setAppliance(Appliance appliance) { this.appliance = appliance; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } }
这样用户并不知道它将使用什么样的电器,即用户对收音机的依赖关系被消除了,这种模式叫做’控制反转’。下面需要做的是在运行时,把用户要使用的Appliance实体类(Radio或Tv)通过set方法放进去,即依赖注入。不过现在先别急,还要实现一个新的类别:电视Tv。它实现Appliance接口。同样地,改造已有的Radio类别,使之实现Applicance接口:
package demo; public class Tv implements Appliance { public String use() { return "TV"; } } package demo; public class RadioV2 implements Appliance{ public String use() { return "RADIO"; } }
工作还没有完,业务逻辑’用户使用收音机’也应该做出修改,变成’用户使用电器’,至于具体使用什么电器,则由主程序在运行时注入,下面让用户分别使用收音机及电视机:
package demo; public class UserPlayAppliance { public static void main(String[] args) { UserV2 user = new UserV2(); user.setUsername("Peter"); RadioV2 radio = new RadioV2(); user.setAppliance(radio); System.out.println(user.getUsername() + " is using " + user.getAppliance().use()); Tv tv = new Tv(); user.setAppliance(tv); System.out.println(user.getUsername() + " is using " + user.getAppliance().use()); } }
跑起来玩一下,看看系统的输出: Peter is using RADIO Peter is using TV 可以看到,用户听了收音机,也看了电视机。在代码中,分别将Radio及Tv赋给user,并且user不关心是什么电器,都可以拿过来play。通过应用IoC及DI模式,使系统附合了OCP法则。User类既保持了它的使用开放性(可以使用新加的Tv类别),也保持了修改的闭合性(新加的Tv类不会导致User类的变化)2 2 还记得在OCP一节中介绍的口诀吗?这里就是 层层之间用接口 的原则体现。 好的,原理讲完了。那么,Spring到底能帮您干些什么呢?简单来说,Spring最最核心的部分就是帮您把代码里user.setAppliance(radio)这样的工作给移到了配置文件中完成了。下面讲讲如何使用Spring。 首先需要在项目中制作一个Spring的配置文件,这个配置文件是XML格式的:
<?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-2.5.xsd"> <bean id="peter" class="demo.UserV2" p:username="Peter" p:appliance-ref='radio' /> <bean id="tom" class="demo.UserV2" p:username="Tom" p:appliance-ref='tv' /> <bean id="radio" class="demo.RadioV2" /> <bean id="tv" class="demo.Tv" /> </beans>
对于这个配置文件,其关键内容是声明了一个名字为peter的bean: <bean id="peter" class="demo.UserV2" p:username="Peter" p:appliance-ref='radio' /> 这个bean为UserV2的实例。两个p元素是将UserV2的两个属性username及appliance注入。配置文件中的’p:username’就相当于调用代码的setUsername()进行属性具体值的注入。Spring读到这个配置文件时,就会调用UserV2中的setUsername方法把属性注入,p:appliance也是一样的道理。 此外,您可以发现,username及appliance的注入方式还稍有不同。由于username是一个String类型的属性,因此我们把需要的值直接写进去就可以了。这里我们指定用户的名字为’Peter’。请注意字串类型的属性值是用双引号括起来的。对于appliance,由于这个属性对应我们定义的Appliance接口,因此我们需要注入一个Appliance的实例。在Spring的配置当中,我们需要注入一个Appliance的bean。因此,在配置文件中我们定义了这个bean: <bean id="radio" class="demo.RadioV2" /> RadioV2是一个实现了Appliance接口的实体类,这个bean的id为radio。在peter中我们将radio这个bean注入到了appliance属性当中。请注意,如果要引用其它的bean,需要在属性后面加’-ref’,并用单引号引用bean的id: p:appliance-ref='radio' 同样的道理,还声明了一个`tom’的用户,不同的是为这个用户注入的电器是Tv而不是Radio。关于Spring配置文件的语法,相信您已经有了一个初步的认识,可能一开始还不习惯,我们随着阅读Spring的配置文件量不断增加以及不断地亲自实践,对它的语法就会慢慢习惯。 此外,配置文件的开始,是Spring的命名空间声明。什么是命名空间呢?您可以把它理解为Java代码中的package。XML文件中的命名空间声明就相当于java代码中对于的package的import。在Java代码中,只有引用了相关的package,才能使用package中提供的类。同样地,只有引入了相关的命名空间,才能使用命名空间中定义的XML元素。在我们的配置文件中,引入了最基本的bean元素,以及简化配置语言的p元素: xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd" 这样,我们在配置文件中就可以使用bean。此外,我们在bean中便可以使用p元素来简化配置。什么叫做’用p元素来简化配置’呢?举个例子您就明白了,比如下面这个声明: <bean id="peter" class="demo.UserV2" p:username="Peter" p:appliance-ref='radio' /> 如果不用p元素,那么上面这个配置默认的Spring配置写法应该是这样的: <bean id="peter" class="demo.UserV2"> <property name="username" value="Peter" /> <property name="appliance" ref="radio" /> </bean> 怎么样,是不是使用p元素后,配置文件简单多了?在配置文件中完成了注入工作后,我们接下来看看如何使用’peter’及’tom’这两个User,下面撰写一个新的UserPlayAppliance类别:
package demo; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class UserPlayApplianceV2 { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext "config.xml"); UserV2 peter = (UserV2) ctx.getBean("peter"); System.out.println(peter.getUsername() + " is using " + peter.getAppliance().use()); UserV2 tom = (UserV2) ctx.getBean("tom"); System.out.println(tom.getUsername() + " is using " + tom.getAppliance().use()); } }
在上面的代码中,首先系统将配置文件 config.xml 读进Spring的系统上下文类别 ApplicationContext 在这里使用了 ClassPathXmlApplicationContext 进行读取工作,从名字就可以看得很清楚这个类从class路径中读取系统配置文件。除此以外,Spring还提供多种加载类,在后续文章中陆续介绍。 在第11行,从ApplicationContext的getBean()方法提取出配置好的用户`peter’,此时Spring会帮您创建配置文件中的bean,即User、Radio,并按照配置,通过调用user的setAppliance()方法把radio注入。15-17行同样道理。跑一下这个程序,看下系统输出: Peter is using RADIO Tom is using TV 以上说明了Spring的基本使用方法及背后所蕴涵的设计思想。在下一节,您将看到如何使用Java 5的最新Annotation特性,简化代码及配置。 Property注入与Constructor注入 我们在前一节通过Spring的配置文件,把某个Bean的某个属性通过此属性的setter方法注入到这个Bean当中。这样的注入方式在Spring称做Property注入。例如: <bean id="tom" class="demo.UserV2" p:username="Tom" p:appliance-ref='tv' /> username与appliance是通过UserV2中的setter方法被注入的:
package demo; public class UserV2 { private String username; private Appliance appliance; ... public void setAppliance(Appliance appliance) { this.appliance = appliance; } public void setUsername(String username) { this.username = username; } }
除了Property注入以外,Spring还支持Constructor注入。即通过建构方法,在Class被创建时,把Bean注入。我们来看看这样的方式。首先创建一个Bean,这个Bean在建构方法接收两个属性的注入:
package demo; public class Bean { private BeanA beanA; private BeanB beanB; public Bean(BeanA beanA, BeanB beanB) { super(); this.beanA = beanA; this.beanB = beanB; } }
下面我们在Spring配置文件中,通过Bean的建构方法把BeanA及BeanB注入: <bean id="bean" class="Bean"> <constructor-arg ref="beanA" /> <constructor-arg ref="beanB" /> </bean> <bean id="beanA" class="BeanA" /> <bean id="beanB" class="BeanB" /> 请注意, contructor-arg 的书写顺序要与建构方法中的参数顺序一致。如果beanA,beanB在 constructor-arg 中的顺序颠倒了,那么配置文件中的beanA将被注入Bean的beanB,配置文件中的beanB将被注入Bean的beanA,导致程序出错,无法执行。 基于Constructor的注入与基于Property的方式在达到的效果和使用目的上没什么区别。但大多数情况下,使用基于Property方法的注入可以使配置文件看起来更加清淅,也不涉及顺序的问题。但使用Constructor方法也有它的好处,如果需要在某个类的建构方法中加入一些与注入息息相关的逻辑代码时,将不得不采取这种方式。在这一方面没什么原则可言。只要您多多使用,找到自己最顺手的方式就可以了。在下一节当中,我们介绍另一种注入方式,也是Spring 2.5中的新方式:基于Java标记的Bean自动绑定。 使用Spring Annotation进行自动绑定 在Java 5中,Annotation特性被标准化,并且允许用户定义自己的标记,Spring利用这一新特征,提供了一系列方便好用的Annotation。 回过头再来看一下上一节的样例。在配置文件中,有如下的声明: <bean id="peter" class="demo.UserV2" p:username="Peter" p:appliance-ref='radio' /> 通过 p:appliance-ref=`radio' ,radio这个bean被注入到user中。为了配合注入,在User类中必须提供getter及setter方法,供Spring使用: private Appliance appliance; public Appliance getAppliance() { return appliance; } public void setAppliance(Appliance appliance) { this.appliance = appliance; } Spring从2.5版本开始支持Java Annotation简化装配流程。下面看一下使用标记的新配置文件及代码。首先是User类别的修改:
package demo; import org.springframework.beans.factory.annotation.Autowired; public class UserV3 { private String username; @Autowired private Appliance appliance; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public Appliance getAppliance() { return appliance; } }
首先是appliance的setter方法被省掉了。在第8行有一个Spring的标记@Autowired。这个标记的作用是将配置文件中,与Appliance同类别的bean自动加载进来。看一下配置文件:
<?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-2.5.xsd"> <context:annotation-config /> <bean id="peter" class="demo.UserV3" p:username="Peter" /> <bean class="demo.RadioV2" /> </beans>
关于这个配置文件,有以下几点要说: •在第9行,新添了一个 context:annotation-config 的元素,这一标记告诉Spring,在代码中使用了Annotation的特性。这样代码中的@Autowired标记才会生效。 •从第11行可以看到,原来的 p:appliance-ref 的设置省去了。因为在代码中appliance属性上声明了@Autowired标记,Spring会在配置中查找与appliance同类别的bean,并进行自动绑定。因此只需声明下面的bean即可。 •在第13行声明了radio bean,它实现了appliance接口,因此会被自动绑定3}。bean的识别id也可以不写了。 3 细心的读者可能会问:如果有多个类都实现了appliance接口,Spring该如何得知绑绑定哪一个实现类?不必着急,请继续往下看。 完成了配置,下面我们来看一下新的逻辑代码:
package demo; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class UserPlayApplianceV3 { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext( "configV2.xml"); UserV3 peter = (UserV3) ctx.getBean("peter"); System.out.println(peter.getUsername() + " is using " + peter.getAppliance().use()); } }
可以看到使用bean的方法没有任何变化,底层的自动绑定并没有影响前端的使用。系统输出如下: Peter is using RADIO 此时会有另一种情况:如果配置文件中有两个bean都实现了appliance接口该怎么办?此时系统该绑定哪一个?答案是系统不能决定,会抛出错误。因此,当配置中有两个相同类别的bean时,需要为其指定类似命名空间的参数,叫做qualifier。下面是一个配置例子:
<?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-2.5.xsd"> <context:annotation-config /> <bean id="peter" class="demo.UserV4" p:username="Peter" /> <bean class="demo.RadioV2"> <qualifier value="radio" /> </bean> <bean class="demo.Tv"> <qualifier value="tv" /> </bean> </beans>
在配置中有两个同属于一个Appliance类别的bean:Radio及Tv,但为它们指定了各自的qualifier。这样,在用户绑定的时候声明使用哪一个,就不会造成混淆:
package demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; public class UserV4 { private String username; @Autowired @Qualifier("radio") private Appliance appliance; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public Appliance getAppliance() { return appliance; } }
下面运行一下业务逻辑:
package demo; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class UserPlayApplianceV4 { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext "configV3.xml"); UserV4 peter = (UserV4) ctx.getBean("peter"); System.out.println(peter.getUsername() + " is using " + peter.getAppliance().use()); } }
这一版代码与前一版没什么区别,仅仅是配置文件使用刚刚做好的的configV3.xml,输出如下: 1 Peter is using RADIO 可以看到,虽然配置中有两个同类别的bean,在qualfier的帮助下,Spring可以正确地绑定需要的bean。这种绑定方式的好处是配置及代码大大地简化了,但付出的代价是User类别只能绑定Appliance某种具体的实现类,如果想从Radio切换到Tv,就需要在 User.java 中将@Qualifier切换至’tv’。这是代码简化付出的代价。 Spring在使用中需要注意的问题 在上一节使用Spring时,用 ClassPathXmlApplicationContext 把配置读取进来,并从中取bean。用下面这个代码看看Spring的这样一个动作的开销是多大:
package demo; import org.springframework.context.support.ClassPathXmlApplicationContext; public class ShowTimeConsumingByLoadingContext { private static long start = 0; private static long end = 0; private static final int TIMES = 100; public static void main(String[] args) { long timer = 0; for (int i = 0; i < TIMES; i++) { start = System.currentTimeMillis(); new ClassPathXmlApplicationContext("config.xml"); end = System.currentTimeMillis(); timer += end - start; } System.out.println("the average time of load is: " + timer / TIMES + "ms"); } }
为了使评测结果更加客观,将计时器分成两个:start使end。并且将其声明为static,在系统加载时为其分配内存,这样声明使两个变量的系统开销影响降至最低。并且在第19行,计时后再计算,减法开销可忽略。最后,使用循环的方式,进行100次的配置加载,最后取平均值。在我的机器上运行结果为为: 40ms。我的机器配置为:内存2GB为667 MHz DDR2 SDRAM、CPU为2.1 GHz Intel Core 2 Duo。可见这个开销是不可以忽视的。 如果在使用Spring时,在代码中有很多这样的加载点,那么每一次用户调用时,都会有一次开销。这在J2EE的世界中是不可接受的。因为WEB应用天生就是多线程的,如果100个用户同时访问您的项目,这时每个线程里的程序类都要执行: 1 new ClassPathXmlApplicationContext("xxx.xml") 那么这个配置就要被加载100次,系统开销是非常大的。因此,您在构建J2EE工程时,必须要避免以这种形式使用Spring,否则它将成为系统瓶颈。正确的方法是在J2EE项目启动时,将所有的配置一次性统一加载到静态内存中,各个线程共享并由Spring控制bean的生命周期。在第二部分将介绍这种加载方式。 小结 本文介绍了OCP原则,DI及Spring的设计模式,并介绍了一种基于Annotation自动绑定方法。请大家记住那条口诀: 层层之间用接口,类的属性要封装