Spring 初探
1. Action 接口:
Action 接口定义了一个 execute 方法,在我们示例中,不同的 Action 实现提供了各自的execute 方法,以完成目标逻辑。
public interface Action {
public String execute(String str);
}
2. Action 接口的两个实现 UpperAction、LowerAction
public class UpperAction implements Action {
private String message;
public String getMessage() {
return message;
}
public void setMessage(String string) {
message = string;
}
public String execute(String str) {
return (getMessage() + str).toUpperCase();
}
}
UpperAction将其message属性与输入字符串相连接,并返回其大写形式。
public class LowerAction implements Action {
private String message;
public String getMessage() {
return message;
}
public void setMessage(String string) {
message = string;
}
public String execute(String str) {
return (getMessage() + str).toLowerCase();
}
}
LowerAction将其message属性与输入字符串相连接,并返回其小写形式。
Spring 配置文件(bean.xml)
<beans>
<description>
Spring Quick Start
</description>
<bean id="TheAction" class="net.xiaxin.spring.qs.UpperAction">
<property name="message">
<value>
HeLLo
</value>
</property>
</bean>
</beans>
测试代码:
public void testQuickStart() {
ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml");
Action action = (Action) ctx.getBean("TheAction");
System.out.println(action.execute("Rod Johnson"));
}
可以看到,上面的测试代码中,我们根据”bean.xml”创建了一个ApplicationContext实
例,并从此实例中获取我们所需的Action实现。
运行测试代码,我们看到控制台输出:
HELLO ROD JOHNSON
我们将bean.xml中的配置稍加修改:
<bean id="TheAction" class="net.xiaxin.spring.qs.LowerAction"/>
再次运行测试代码,看到:
……
hello rod johnson
仔细观察一下上面的代码,可以看到:
1. 我们的所有程序代码中(除测试代码之外) ,并没有出现Spring中的任何组件。
2. UpperAction和LowerAction的Message属性均由Spring通过读取配置文件(bean.xml)动态设置。
3. 客户代码(这里就是我们的测试代码)仅仅面向接口编程,而无需知道实现类的具体名称。同时,我们可以很简单的通过修 改配置文件来切换具体的底层实现类。
上面所说的这些,对于我们的实际开发有何帮助?
组件并不需要实现框架指定的接口,因此可以轻松的将组件从Spring中脱离,甚至不需要任何修改(这在基于EJB框架实现的应用中是难以想象的) 。
其次,组件间的依赖关系减少,极大改善了代码的可重用性。
Spring的依赖注入机制,可以在运行期为组件配置所需资源,而无需在编写组件代码时就加以
指定,从而在相当程度上降低了组件之间的耦合。
上面的例子中,我们通过Spring,在运行期动态将字符串 “HeLLo” 注入到Action实现类的
Message属性中。
Spring通过依赖注入模式,将依赖关系从编码中脱离出来,从而大大降低了组件之间的耦合,实现了组件真正意义上的即插即用。这也是Spring最具价值的特性之一。
- 面向接口编程。
Spring使得接口的定义和使用不再像传统编码过程中那么繁琐(传统编码过程中,引入一个接口,往往也意味着同时要引入一个Factory类,也许还有一个额外的配置文件及其读写代码) 。
Spring是一个从实际项目开发经验中抽取的,可高度重用的应用框架。
Spring Framework中目前最引人注目的,也就是名为控制反转(IOC =Inverse Of Control)或者依赖注入(DI =Dependence Injection)的设计思想,这的确是相当优秀的设计理念。
Spring 基础语义
IoC,用白话来讲,就是由容器控制程序之间的关系,而非传统实现中,由程序代码直接操控。这也就是所谓“控制反转”的概念所在:控制权由应用代码中转到了外部容器,控制权的转移,是所谓反转。
从名字上理解,所谓依赖注入,即组件之间的依赖关系由容器在运行期决定,形象的来说,即由容器动态的将某种依赖关系注入到组件之中。
回顾 Quick Start 中的示例,UpperAction/LowerAction 在运行前,其 Message 节点为空。运行后由容器将字符 串“HeLLo”注入。此时 UpperAction/LowerAction 即与内存中的“HeLLo”字符串对象建立了依赖关系。也许区区一个字符串我们无法感受出依赖关系的存在。如果把这里的 Message 属性换成一个数据源(DataSource) ,可能更有感觉:
<beans>
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName">
<value>
java:comp/env/jdbc/sample
</value>
</property>
</bean>
<bean id="SampleDAO" class="net.xiaxin.spring.dao.SampleDAO">
<property name="dataSource">
<ref local="dataSource" />
</property>
</bean>
</beans>
其中SampleDAO中的dataSource将由容器在运行期动态注入, 而DataSource的具体配置和初始化工作也将由容器在运行期完成。
对比传统的实现方式(如通过编码初始化DataSource实例) ,我们可以看到,基于依赖注入的系统实现相当灵活简洁。
通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定SampleDAO中所需的DataSource实例。SampleDAO只需利用容器注入的DataSource实例,完成自身的业务逻辑,而不用
关心具体的资源来自何处、由谁实现。
上面的实例中,我们假设SampleDAO是一个运行在J2EE容器中的组件(如 Weblogic) 。在运行期,通过JNDI从容器中获取DataSource实例。
现在假设我们的部署环境发生了变化,系统需要脱离应用服务器独立运行,这样,由于失去了容器的支持,
原本通过JNDI获取DataSource的方式不再有效。我们需要如何修改以适应新的系统环境?很简单,我们
只需要修改dataSource的配置:
<beans>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>
org.gjt.mm.mysql.Driver
</value>
</property>
<property name="url">
<value>
jdbc:mysql://localhost/sample
</value>
</property>
<property name="username">
<value>
user
</value>
</property>
<property name="password">
<value>
mypass
</value>
</property>
</bean>
<bean id="SampleDAO" class="net.xiaxin.spring.dao.SampleDAO">
<property name="dataSource">
<ref local="dataSource" />
</property>
</bean>
</beans>
这里我们的DataSource改为由Apache DBCP组件提供。没有编写任何代码我们即实现了DataSource的切换。回想传统编码模式中,如果要进行同样的修改,我们需要付出多大的努力。
依赖注入机制减轻了组件之间的依赖关系,同时也大大提高了组件的可移植性,这意味着,组件得到重用的机会将会更多。
依赖注入的几种实现类型
Type1 接口注入
我们常常借助接口来将调用者与实现者分离。
public class ClassA {
private InterfaceB clzB;
public doSomething() {
Ojbect obj = Class.forName(Config.BImplementation).newInstance();
clzB = (InterfaceB) obj;
clzB.doIt()
}……
}
上面的代码中,ClassA依赖于InterfaceB的实现,如何获得InterfaceB实现类的实例?传统的方法是在代码中创建InterfaceB实现类的实例,并将起赋予clzB。
而这样一来,ClassA在编译期即依赖于*InterfaceB的实现。为了将调用者与实现者在编译期分离*,于是有了上面的代码,我们根据预先在配置文件中设定的实现类的类名(Config.BImplementation),动态加载实现类,并通过InterfaceB强制转型后为ClassA所用。这就是接口注入的一个最原始的雏形。
而对于一个Type1型IOC容器而言,加载接口实现并创建其实例的工作由容器完成。
如下面这个类:
public class ClassA {
private InterfaceB clzB;
public Object doSomething(InterfaceB b) {
clzB = b;
return clzB.doIt();
}……
}
在运行期,InterfaceB实例将由容器提供。
Type1型IOC发展较早(有意或无意) ,在实际中得到了普遍应用,即使在IOC的概念尚未确立时,这样的方法也已经频繁出现在我们的代码中。
Type2 设值注入
在各种类型的依赖注入模式中,设值注入模式在实际开发中得到了最广泛的应用(其中很大一部分得力于Spring框架的影响)。
在笔者看来,基于设置模式的依赖注入机制更加直观、也更加自然。Quick Start中的示例,就是典型的设置注入,即通过类的setter方法完成依赖关系的设置。
Type3 构造子注入
构造子注入,即通过构造函数完成依赖关系的设定,如:
public class DIByConstructor {
private final DataSource dataSource;
private final String message;
public DIByConstructor(DataSource ds, String msg) {
this.dataSource = ds;
this.message = msg;
}……
}
可以看到,在Type3类型的依赖注入机制中,依赖关系是通过类构造函数建立,容器通过调用类的构造方法,将其所需的依赖关系注入其中。
Spring Bean 封装机制
Spring 从核心而言,是一个 DI 容器,其设计哲学是提供一种无侵入式的高扩展性框架。即无需代码中涉及 Spring 专有类,即可将其纳入 Spring 容器进行管理。
为了避免这种情况,实现无侵入性的目标。Spring 大量引入了 Java 的 Reflection 机制,通过动态调用的方式避免硬编码方式的约束,并在此基础上建立了其核心组件 BeanFactory,以此作为其依赖注入机制的实现基础。
org.springframework.beans 包中包括了这些核心组件的实现类, 核心中的核心为 BeanWrapper和 BeanFactory 类。
所谓依赖注入,即在运行期由容器将依赖关系注入到组件之中。讲的通俗点,就是在运行期,由Spring根据配置文件,将其他对象的引用通过组件的提供的setter方法进行设定。
我们知道,如果动态设置一个对象属性,可以借助Java的Reflection机制完成:
Class cls = Class.forName("net.xiaxin.beans.User");
Method mtd = cls.getMethod("setName", new Class[] {
String.class
});
Object obj = (Object) cls.newInstance();
mtd.invoke(obj, new Object[] {
"Erica"
});
return obj;
Spring BeanWrapper基于同样的原理,提供了一个更加完善的实现。
看看如何通过Spring BeanWrapper操作一个JavaBean:
Object obj = Class.forName("net.xiaxin.beans.User").newInstance();
BeanWrapper bw = new BeanWrapperImpl(obj);
bw.setPropertyValue("name", "Erica");
System.out.println("User name=>"+bw.getPropertyValue("name"));
通过BeanWrapper,我们可以无需在编码时就指定JavaBean的实现类和属性值,通过在配置文件加以设定,就可以在运行期动态创建对象并设定其属性(依赖关系) 。
ApplicationContext
ApplicationContext覆盖了BeanFactory的所有功能,并提供了更多的特性。此外,
ApplicationContext为与现有应用框架相整合,提供了更为开放式的实现(如对于Web应用,我们可以在
web.xml中对ApplicationContext进行配置) 。
对于Web应用,Spring提供了可配置的ApplicationContext加载机制。
加载器目前有两种选择:ContextLoaderListener和ContextLoaderServlet。这两者在功能上完全等同,只是一个是基于Servlet2.3版本中新引入的Listener接口实现,而另一个基于Servlet接口实现。开发中可根据目标Web容器的实际情况进行选择。
配置非常简单,在web.xml中增加:
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
或
<servlet>
<servlet-name>
context
</servlet-name>
<servlet-class>
org.springframework.web.context.ContextLoaderServlet
</servlet-class>
<load-on-startup>
1
</load-on-startup>
</servlet>
通过以上配置,Web容器会自动加载/WEB-INF/applicationContext.xml初始化ApplicationContext实例,如果需要指定配置文件位置,可通过context-param加以指定:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/myApplicationContext.xml</param-value>
</context-param>
配置完成之后,即可通过WebApplicationContextUtils.getWebApplicationContext方法在Web应用中获取ApplicationContext引用。
Spring 高级特性 Web 应用与 MVC
从较偏向设计的角度出发,WebWork2 的设计理念更加先进,其代码与 Servlet API 相分离,这使得单元测试更加便利,同时系统从 BS 结构转向 CS 接口也较为简单。另外,对于基于模板的表现层技术(Velocity、Freemarker 和 XSLT)的支持,也为程序员提供了除 JSP之外的更多的选择(Struts 也支持基于模板的表现层技术,只是实际中不太常用)。
而对于 Spring 而言,首先,它提供了一个相当灵活和可扩展的 MVC 实现,与 WebWork2相比,它在依赖注入方面、AOP 等方面更加优秀,但在 MVC 框架与底层构架的分离上又与Webworks 存在着一定差距(Spring 的 MVC 与 Servlet API 相耦合,难于脱离 Servlet容器独立运行,在这点的扩展性上,比 Webwork2 稍逊一筹)。
我们还要注意到,Spring 对于 Web 应用开发的支持,并非只限于框架中的 MVC 部分。即使不使用其中的 MVC 实现, 我们也可以从其他组件, 如事务控制、 ORM 模板中得益。 同时, Spring也为其他框架提供了良好的支持,如我们很容易就可以Struts 与 Spring 甚至 WebWork与 Spring 搭配使用(与 WebWork 的搭配可能有些尴尬,因为两者相互覆盖的内容较多,如
WebWork 中的依赖注入机制、AOP 机制等与 Spring 中的实现相重叠)。因此,对于 Spring在 Web 应用中的作用,应该从一个更全面的角度出发。
对于现有较成熟的 Model-View-Control(MVC)框架而言,其解决的主要问题无外乎下面几部分:
1. 将 Web 页面中的输入元素封装为一个(请求)数据对象。
2. 根据请求的不同,调度相应的逻辑处理单元,并将(请求)数据对象作为参数传入。
3. 逻辑处理单元完成运算后,返回一个结果数据对象。
4. 将结果数据对象中的数据与预先设计的表现层相融合并展现给用户。
下面的实例,实现了一个常见的用户登录逻辑,即用户通过用户名和密码登录,系统对用户名和密码进行检测,如果正确,则在页面上显示几条通知信息。如果登录失败,则返回失败界面。
index.html:
<html>
<body>
<form method="POST" action="/login.do">
<p align="center">
登录
</p>
<br>
用户名:
<input type="text" name="username">
<br>
密 码 :
<input type="password" name="password">
<br>
<p>
<input type="submit" value="提交" name="B1">
<input type="reset" value="重置" name="B2">
</p>
</form>
</body>
</html>
很简单的一个登录界面,其中包含了一个用以输入用户名密码的 form,针对此 form 的提交将被发送到”/login.do”
MVC 关键流程的第一步,即收集页面输入参数,并转换为请求数据对象。这个静态页面提供了一个基本的输入界面,下面这些输入的数据将被发送至何处,将如何被转换为请求数据对象?
现在来看接下来发发生的事情:
当用户输入用户名密码提交之后,此请求被递交给 Web 服务器处理,上面我们设定 form提交目标为”/login.do”,那么 Web 服务器将如何处理这个请求?显然,标准 Http 协议中,并没有以.do 为后缀的服务资源,这是我们自己定义的一种请求匹配模式。此模式在 web.xml 中设定:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4">
<servlet>
⑴
<servlet-name> Dispatcher</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name> contextConfigLocation </param-name>
<param-value>/WEB-INF/Config.xml </param-value>
</init-param>
</servlet>
<servlet-mapping>
⑵
<servlet-name> Dispatcher </servlet-name>
<url-pattern> *.do </url-pattern>
</servlet-mapping>
</web-app>
⑴ Servlet 定义
这里我们定义了请求分发 Servlet,即:
org.springframework.web.servlet.DispatcherServlet
DispatcherServlet 是 Spring MVC 中负责请求调度的核心引擎,所有的请求将由此 Servlet 根据配置分发至各个逻辑处理单元。其内部同时也维护了一个ApplicationContext 实例。
我们在节点中配置了名为“contextConfigLocation”的Servlet 参数, 此参数指定了 Spring 配置文件的位置 “/WEB-INF/Config.xml”。
果忽略此设定,则默认为“/WEB-INF/-servlet.xml”,其中 以 Servlet 名 替 换 ( 在 当 前 环 境 下 , 默 认 值 也 就 是“/WEB-INF/Dispatcher-servlet.xml)。
⑵ 请求映射
我们将所有以.do 结尾的请求交给 Spring MVC 进行处理。当然,也可以设为其他值,
如.action、.action 等。
通过以上设定,Web 服务器将把登录界面提交的请求转交给 Dispatcher 处理,Dispatcher 将提取请求(HttpServletRequest)中的输入数据,分发给对应的处理单元,各单元处理完毕后,将输出页面返回给 Web 服务器,再由 Web 服务器返回给用户浏览器。
Dispatcher 根据什么分发这些请求?显然,我们还需要一个配置文件加以设定。这也就是上面提及的 Config.xml,此文件包含了所有的“请求/处理单元”关系映射设定,以及返回时表现层的一些属性设置