2.2 绝不重造轮子:Spring
设计Spring框架的初衷是简化Java EE应用的开发过程。但Spring的大多数功能并未提供自己的实现,而是整合了众多的第三方的技术。比如在持久化功能方面整合了Hibernate、EJB、JDO等技术,在AOP功能方面虽然提供了Spring AOP技术,但仍然可以整合像AspectJ这样的第三方AOP技术。Spring通过整合大量的第三方框架,大大简化了这些第三方技术的使用难度。因此,Spring的设计理念就是尽可能地整合第三方的技术,使得这些被整合的技术更容易使用,更易维护,从而大大降低程序开发的难度。除非有必要,否则Spring绝不重新实现这些技术,也就是“绝不重造轮子”。
2.2.1 Spring与IOC模式
装配JavaBean是Spring框架的核心技术。在传统的程序中,往往直接使用new关键字来创建JavaBean的对象实例,并在程序中为JavaBean的属性赋值。这样做就意味着将JavaBean与调用该JavaBean的类牢牢地绑定到一起,也就是说,它们的耦合度太高。如果需要改变设置JavaBean属性的代码,那么就必须修改调用JavaBean的源代码。
虽然修改源代码也并不是不可以,但理想的实现方法是尽量不修改已经编写完的源代码,而采用扩展或修改配置文件的方法改变系统的行为。这也就是“对修改关闭,对扩展开放”的设计原则。为此,Spring提供了一种机制,使得创建JavaBean以及设置JavaBean属性的工作可以通过配置文件以及Spring框架本身来完成。这样一来,当某些地方需要改变时,修改Spring的配置文件即可。这个过程实际上就是Spring框架通过读取相应的配置文件中的内容,并根据这些配置自动装载(创建JavaBean对象)和配置JavaBean的属性。因此,也可以称这个过程为“装配JavaBean”。
JavaBean的属性类型有可能是开发人员自定义的类。在这种情况下,Spring建议为这些类提供接口(或抽象类),同时这些类实现这些接口。而JavaBean的相应属性的类型应该为类所实现的接口,而不应该直接使用类。如下面的代码所示:
interface MyInterface { public void myMethod(); } class MyClass1 implements MyInterface { public void myMethod(){ ... } } class MyClass2 implements MyInterface { public void myMethod(){ ... } } // 要被Spring装配的JavaBean public class MyJavaBean
{ private MyInterface myInterface; public MyInterface getMyInterface() { return myInterface; } public void setMyInterface(MyInterface myInterface) { this.myInterface = myInterface; } }
从上面的代码可以看出,MyJavaBean类中的myInterface属性的数据类型是MyInterface。而在装配MyJavaBean时,需要为myInterface属性指定一个实际的类(MyClass1或MyClass2)的对象实例。这样做的好处很多。例如,如果MyJavaBean类在开发过程中需要对myInterface属性进测试,这时就可以为myInterface属性指定一个用于测试的对象实例。当正式发布MyJavaBean类时,需要重新为myInterface属性指定一个用于发布的对象实例。在这种情况下,就可以编写两个实现MyInterface接口的类,一个用于测试(假设为MyClass1),一个在实际发布时使用(假设为MyClass2)。
如果使用Spring框架来装配MyJavaBean,那么在切换这两个类时,就不需要修改源代码了,而只需要修改Spring的配置文件即可。也就是说,MyClass1、MyClass2与MyJavaBean的耦合度非常低,或者说是通过MyInterface接口降低了MyClass1、MyClass2与MyJavaBean的耦合度。当然,也可以将接口换成抽象类,这要根据具体的情况而定。这种通过接口或抽象类降低JavaBean与其他类之间的耦合度的方式也称为IOC(Inversion of Control,反转控制)模式。IOC模式也是策略(Strategy)模式的基础。
注意
在本节及后面的章节会经常提到JavaBean。实际上,JavaBean也是普通的Java类,之所以将某些Java类称为JavaBean,是因为这些Java类会经常被当成组件使用到。也就是说,可以将被当成组件的经常使用到的Java类称为JavaBean。作为JavaBean组件,就不可避免地要拥有一些属性。但Java语言中并没有定义属性的语法,因此,JavaBean规范将一个属性分为getter和setter方法。例如,如果要获得JavaBean的name属性的值,就需要调用该属性的getter方法(getName),如果要为name属性赋值,就需要调用该属性的setter方法(setName)。因此,JavaBean的另一个特征就是在JavaBean中都会有一对或多对getter和setter方法。就像上面代码中的MyJavaBean类一样,因此,也可以将MyJavaBean类称为JavaBean。
2.2.2 Spring最新版的下载与安装
在笔者写本书时,Spring的最新版本是Spring 2.5.6,读者可以从如下的地址下载Spring的最新版本:
http://www.springsource.org/download
在下载完Spring的压缩包后,将其解压。在<Spring解压目录>\dist目录中有一个spring.jar文件。将该文件复制到WEB-INF\lib目录中。在2.2.3节将介绍使用Spring的具体方法。
2.2.3 Struts 2和Spring整合的原理
在2.1.3节的例子可以看出。在CalcAction类中直接创建了Addition和Subtraction类的对象实例,并调用了相应的方法来执行业务逻辑。那么在看了2.2.1节介绍的IOC模式后,我们会发现这种调用方式会使CalcAction类和Addition及Subtraction之间的耦合度过高。如果需要将Addition类切换到Subtraction类,或进行相反的切换,就需要修改CalcAction类的代码。因此,使用Spring来自动创建业务逻辑类的对象实例,并降低它们的耦合度是一个非常不错的选择。
最理想的方式就是,当Struts 2创建CalcAction类的对象实例时就自动创建业务逻辑类的对象实例。而在CalcAction类中可以使用业务逻辑类实现的接口来引用相应的对象实例。但是有一个问题,就是如何让Struts 2在创建CalcAction类的对象实例时就自动创建Additon和Subtraction类的对象实例呢?当然,方法可能比较多。在Struts 2的发行包中提供了一个Struts 2插件,可以非常方便地解决这个问题。这个Struts 2插件的jar文件是struts2-spring-plugin-2.1.6.jar,在使用该插件之前,需要先将该文件复制到WEB-INF\lib目录中。
在使用Spring之前,需要先在web.xml文件中配置一个监听器,代码如下:
<listener> <listener-class>org.springframework.web.context.ContextLoaderListener </listener-class> </listener>
在完成上面的工作后,需要在CalcAction中定义属性,例如,定义一个Addition类型的属性,代码如下:
public class CalcAction { private Additon addition; public Addition getAddition() { return this.addition; } public void setAddition(Addition addition) { this.addition = addition; } … }
如果在Spring中的装配文件(默认为applicationContext.xml)中使用<bean>元素来装配CalcAction,当id属性值为addition时,Struts 2会通过上述的插件自动装配CalcAction,并将装配后的对象实例赋给CalcAction类的addition属性。在默认情况下是按属性名来匹配的,也就是说,当某个<bean>元素的id属性值与Action类中的某一个属性名相同时,系统会自动装配该<bean>元素所指定的JavaBean,并将装配后的对象赋给Action类的相应属性(这时该属性必须有setter方法)。要注意的是,这时<bean>元素的class属性所指的类必须与Action类中相应属性的数据类型相同,或是该数据类型(类或接口)的子类。
实际上,自动为Action类相应属性赋值的功能是通过Struts 2的另外一个autowiring插件完成的。该插件在默认情况下通过将Action类的属性名和<bean>元素的id属性值进行匹配的方式,来为Action类的相应属性赋值。
如果想改变这个默认的设置,例如,使用按类型匹配的方式为Action类的属性赋值,可以在struts.xml文件中设置autowiring插件的autowireStrategy参数。该参数的值是一个int类型。该参数值从1至4分别对应于Spring的4种自动装配方式。1表示按名称装配;2表示按类型装配;3表示按构造方法来装配;4表示以自动的方式装配。相对应的常量可以查看如下的接口:
org.springframework.beans.factory.config. AutowireCapableBeanFactory
要想改变autowiring插件的装配方式,首先要在struts.xml文件中配置一个新的拦截器栈,代码如下:
<interceptors> <interceptor-stack name="extStack"> <! -- autowiring未在defaultStack中引用,因此,必须显示来引用该拦截器 --> <interceptor-ref name="autowiring"> <! -- 设置按类型进行装配 --> <param name="autowireStrategy">2</param> </interceptor-ref> <interceptor-ref name="defaultStack" /> </interceptor-stack> </interceptors> 接下来就可以在相应的<action>元素中引用该拦截器栈了,代码如下: <action name="calc" class="net.blogjava.nokiaguy.actions.CalcAction"> <interceptor-ref name="extStack" /> <result name="success">/WEB-INF/springcalc.jsp</result> </action>
在进行完上面的设置后,在applicationContext.xml文件中装配Addition和Subtraction类时,<bean>元素的id值可以任意设置,只要class属性值符合上述的约定即可。
除了上述的方法可以改变自动装配方式外,还可以通过在struts.xml文件中设置常量的方式来改变自动装配的方式。下面的配置代码将自动装配方式设置成了按类型自动装配:
<constant name="struts.objectFactory.spring.autoWire" value="type"/>
value属性值除了type外,还可以是name(按名字自动装配)、constructor(按构造方法自动装配)、auto(自动侦测方式的自动装配)。
在2.2.4节会用一个完整的例子来演示如何整合Struts 2和Spring。
2.2.4 用整合Struts 2和Spring的方式重新实现计算加减法的Web程序
在本节将重新实现2.1.3节的计算加减法的Web程序,不过用的是Struts 2和Spring整合的方式,读者从该例子中可以充分了解到如何通过Spring进行接口解耦合,以及如何方便地切换两个业务逻辑(Addition和Subtraction类)。
1 编写Calculator接口
由于本例使用了IOC模式,因此,需要用一个接口作为JavaBean方法的参数类型。Calculator接口的代码如下:
package net.blogjava.nokiaguy.models.interfaces; public interface Calculator { public int calc(int x, int y); public void setMessage(String s); public String getMessage(); }
其中calc方法是用来计算加法或减法的,而setMessage和getMessage方法用来设置和获得表示计算结果的字符串。
2 编写CalculatorMessage类
由于设置和获得表示计算结果信息的代码在不同的业务逻辑类中是相同的,因此,需要将这些代码提出来放在CalculatorMessage类中,然后由业务逻辑类从CalculatorMessage类继承。CalculatorMessage类的实现代码如下:
package net.blogjava.nokiaguy.models; import java.text.MessageFormat; import net.blogjava.nokiaguy.models.interfaces.Calculator; public class CalculatorMessage implements Calculator { private String msg; protected int x, y; protected int value; // 在calc方法中将两个操作数(x和y)及计算结果(value)保存在类变量中, // 以便getMessage方法可以使用这三个值 public int calc(int x, int y) { this.x = x; this.y = y; return value; } @Override public String getMessage() { // 使用MessageFormat的format方法为三个占位符赋值,这三个占位符分别是x、y和value return MessageFormat.format(msg, x, y, value); } @Override
public void setMessage(String s) { msg = s; } }
3 编写AdditionCalc和SubtractionCalc类。
为了与2.1.3中的例子进行对比,在本例中新建两个类来分别完成计算加法和减法的工作。
AdditionCalc类的代码如下:
package net.blogjava.nokiaguy.models; public class AdditionCalc extends CalculatorMessage { @Override public int calc(int x, int y) { value = x + y; return super.calc(x, y); } }
SubtractionCalc类的代码如下:
package net.blogjava.nokiaguy.models; import net.blogjava.nokiaguy.models.interfaces.Calculator; public class SubtractionCalc extends CalculatorMessage { @Override public int calc(int x, int y) { value = x - y; return super.calc(x, y); } }
4 编写SpringCalcAction类。
SpringCalcAction是一个Action类,与2.1.3节的CalcAction类的功能类似,只不过利用了Struts 2插件自动装配Action类属性的功能来获得业务逻辑对象。SpringCalcAction类的代码如下:
package net.blogjava.nokiaguy.actions; import net.blogjava.nokiaguy.models.interfaces.Calculator; public class SpringCalcAction { private int operand1; private int operand2; // calculator属性由Struts 2插件自动装配,在Spring配置文件中需要有同名的<bean>元素 private Calculator calculator; ... ... // 此处省略了属性的getter和setter方法 public String execute()
{ // 只需要调用Calculator对象的calc方法,在切换业务模型时,不需要修改下面的代码 int value = calculator.calc(operand1, operand2); return "success"; } }
5 配置SpringCalcAction类。
SpringCalcAction类的配置代码如下:
<action name="springcalc" class="net.blogjava.nokiaguy.actions.SpringCalcAction"> <! -- <interceptor-ref name="extStack" />--> <result name="success">/WEB-INF/springcalc.jsp</result> </action>
如果将<interceptor-ref>元素的注释去掉,Struts 2在创建SpringCalcAction类的对象实例时就会采用类型匹配的方式来装配SpringCalcAction类中的属性。extStack拦截器栈的配置见2.2.3节的相关内容。
6 编写装配代码。
Spring插件在默认情况下会读取WEB-INF\applicationContext.xml文件中的内容。开发人员可以在该文件中配置相应的装配代码(标准的XML格式代码)。当需要装配的JavaBean太多时,可以在applicationContext.xml文件中使用<import>元素引用其他的装配文件。也可以在web.xml文件中设置contextCongifLocation参数来指定其他的装配文件,代码如下:
<context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/calculator.xml, /WEB-INF/applicationContext-*.xml</p aram-value> </context-param>
如果指定了多个装配文件,中间用逗号(, )分隔。装配文件名可以使用通配符,如/WEB-INF/applicationContext-*.xml表示所有以applicationContext开头的,并且文件扩展名为xml的装配文件。
本例在applicationContext.xml文件中使用<import>元素来引用其他的装配文件。在WEB-INF目录中建立一个applicationContext.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:aop="http:// www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframe work.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework. org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/tx http://www.springframework.
org/schema/tx/spring-tx-2.5.xsd"> <! -- 导入用于装配SpringCalcAction类的相关属性的装配文件 --> <import resource="calculator.xml"/> </beans>
在上面的代码中使用<import>元素引用了一个calculator.xml文件,在该文件中配置了装配业务逻辑类的代码。在WEB-INF目录中建立一个calculator.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:aop="http://www. springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframe work.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework. org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/tx http://www.springframework. org/schema/tx/spring-tx-2.5.xsd"> <! -- 装配AdditionCalc类 --> <bean id="calculator" class="net.blogjava.nokiaguy.models.AdditionCalc"> <property name="message"> <value>{0}+{1}={2}</value> </property> </bean> </beans>
在上面的配置代码中将AdditionCalc类的message属性设为“{0}+{1}={2}”,其中{n}为占位符,从0开始。{0}表示操作数1; {1}表示操作数2; {2}表示计算结果。在CalculatorMessage类的getMessage方法中使用了MessageFormat类的format方法为这三个占位符赋值。代码详见2。如果想切换到SubtractionCalc类,需要将上面代码中的<bean>元素的id属性改成其他的值,或将<bean>元素注释掉,然后再加入如下的装配代码:
<bean id="calculator" class="net.blogjava.nokiaguy.models.SubtractionCalc"> <property name="message"> <value>{0}-{1}={2}</value> </property> </bean>
如果采用了按类型自动装配的方式,则id属性的值就变得不重要了,而要想切换业务逻辑类,就需要直接修改class属性的值了。
7 编写springcalc.jsp页面。
springcalc.jsp页面与calc.jsp页面类似,但有如下两点不同:
· <s:form>标签的action属性值变为了springcalc。
· 需要使用<s:property value="calculator.message"/>来显示计算结果。
在测试springcalc.jsp页面后,会得到与图2.1和图2.2相同的效果。