这一章包含下面几个主题:
对于Java开发者来说,这是一个好的时代。
在过去的20年中,Java经历了好的时候,也经历了坏的时候。尽管有一些粗糙的地方,比如:Applets、
EJB、JDO和无数的日志框架,Java有丰富多样的历史,有很多企业已经建立的平台。其中,Spring一直
都是其中最重要的组成部分。
在早期,Spring被创建用于替代笨重的Java企业技术,比如EJB。相比于EJB,Spring提供了一个更加精
简的编程模型。它提供了简单Java对象(POJO)更大的权力,相对于EJB及其他Java企业规范。
随着时间的推移,EJB及Java企业规范2.0版本本身也提供了一个简单的POJO模型。现在,EJB的一些概
念,如DI和AOP都来自于Spring。
尽管现在J2EE(即总所周知的JEE)能够赶上Spring,但是Spring从未停止演进。即使是现在,Spring开始进步的时候,
J2EE都是开始在探索,而不是创新。移动开发、社交API的集成、NoSql数据库、云计算和大数据,仅仅是Spring创新
的一些方面。而且未来,Spring会继续发展。
就像我说的,对于Java程序员来说,这是一个好的时代。
这是一本关于Spring的书。在这一章中,我们会概述Spring,为你带来一个简单Spring概述。这一章将给你
一个Spring解决问题的方法,本书的其余部分会深入学习。
Spring是由Rod Johnson开发的开源框架,该框架最初在他的书:Expert One-on-One: J2EE Design and
Development (Wrox, 2002, http://amzn.com/0764543857) 中提出。Spring是为了解决企业开发的复杂
任务Java程序都可以从其简单性、可测试性和松耦合中获得好处。
Bean或者其他的名称,虽然Spring使用Bean或者JavaBean指代应用组件,但是这不意味着Bean组件必须遵
循JavaBeans规范。一个Spring Bean必须是一个POJO类型。在这本书中,我假定JavaBean的宽松定义就是POJO。
就像你在本书中看到的,Spring可以做很多事情。但是几乎所有Spring提供的理念、都集中在其根本使命,
即简化Java开发。
这是一个大胆的声明,某些框架声称可以简化某一些事情,但是Spring可以简化Java广阔的开发。这个需要更
多的解释。Spring怎么简化Java开发。
为了简化Java开发,Spring引入了下面4各核心策略:
基本上,Spring能做的绝大多数事情都可以追溯到上面的4个策略的某一方面或者几个方面。在本章的其余部分,
我将会对上面的四个方面进行展开分析,并且通过例子说明,Spring是怎么简化Java开发的。
如果你做了一段时间的Java开发,你就会发现,很多框架需要你去实现预定义的类或者实现预定义的接口,
这种侵入性的编程模型的最简单例子就是EJB-2的无状态会话Bean。但是,即使是这么一个容易实现的目
标,在Struts的早期版本、Webwork、 Tapestry和许多其他的Java规范和框架中都很容易见到。
Spring尽量避免它自己的API去污染你的代码,它绝不会强迫你去实现一个Spring标准的接口或者继承一个
Spring标准的类。相反,基于Spring的应用开发通常没有一个明显的标志说他们是基于Spring的。有时,一
个被Spring注解注释的类可能也被其他的注解注释。
为了描述这种情况,请考虑下面的HelloworldBean的例子:
//Spring并不会对HelloWoldBean做出任何要求
package com.habuma.spring;
public class HelloWorldBean{
public String sayHello(){
return "Hello World"; <===这个就是所有需要的东西
}
}
如你所见,这是一个简单且普通的Java类,也就是一个POJO。没有任何特殊说明,指明它是一个Spring的
组件。Spring的非侵入性编程模型表明,该类可以在Spring中使用,也可以在其他的应用程序中使用。
尽管POJO的形式简单,但是它的力量是巨大的。Spring提高POJO力量的一种方式就是使用DI去组装它们。
让我们看看DI是怎么帮助应用程序组件之间进行解耦的。
DI这个词刚听起来觉得是害怕的,它可能是相当复杂的编程技术或者设计模式。但事实证明,DI一点都不像
它听起来那么难。通过在应用中使用DI,你会发现你的应用程序变淡简单、容易理解并且易于测试。
一个正常的应用程序都是有两个或者更多个相互协作的类组合起来的。传统上,每个对象都会保存它所以来的
对象的引用。这个会导致高度耦合并且难于测试。
并入,考虑下面的Knight类:
// RescueDamselQuest的quest对象只能应用在DamselRescuingKnight中
package com.springinaction.knights;
public class DamselRescuingKnight implements Knight{
private RescueDamselQuest quest;
public DamselRescuingKnight(){
this.quest = new RescueDamselQuest();
}
public void embarkOnQuest(){
quest.embark();
}
}
如上所示,骑士创建了一个少妇需要营救的请求(RescueDamselQuest)在它自己的构造函数中。这个会
使骑士与少妇请求绑定到一起,这严重限制了骑士的能力。如果一个少妇需要营救,那没有问题。但是如果
一头大象需要被杀死,那么骑士什么都做不了,只能坐在旁边观看。
更难的是,如果对该类写单元测试是很难得。在这个测试中,你可能需要断言,当骑士的embarkOnQuest
被调用时,那个请求embark方法也被调用,但是这里没有什么函数能够完成这个任务。不幸的是,DamselRescuingKnight是不可测试的。
耦合是一个特别糟糕的主意,一方面,耦合的代码难于测试、难于重用、难以理解并且他经常导致“打地鼠”的Bug行为(一种修改一个Bug通常会引起其他新的一个甚至更多的新bug的行为)。另一方面,一定
数量的耦合代码是必须的,完全不耦合的代码将什么事情都不做。为了去做一些有用的事情,类需要知道彼此。耦合是必须的,但是必须被小心的管理。
使用DI,对象在创建的时候被一些确定系统对象坐标的第三方去给予出其依赖,对象不需要去创建或者获取
其依赖,像下图描述的那样,依赖被注入进了需要他们的对象。
为了描述这个要点,请看下面BraveKnight的例子:该骑士不仅勇敢,而且当任何一种请求来临是都可以解
决。
//一个灵活的骑士
package com.springinaction.knights;
/** * Created by Robert.wang on 2016-3-9-0009. */
public class BraveKnight implements Knight {
private Quest quest;
public BraveKnight(Quest quest) {
this.quest = quest;
}
@Override
public void embarkOnQuest() {
quest.embark();
}
}
就像在上面看到的一样,BraveKnight不像DamselRescuingKnight 一样创建自己的Quest,而是在构造函
数的参数中传入Quest,这样的DI就是著名的构造函数注入(Constructor injection)。
更加的,那个Quest只是一个接口,所有实现该接口的实现都可以传入。所以BraveKnight可以处理不同的
需求。
关键点就是BraveKnight没有跟任何特定的Quest进行绑定。它不在乎是什么样的请求,只要该请求实现了Quest接口就可以。这个就是DI的好处–松耦合。如果一个对象的依赖只是一个接口,那么你可以将他的实
现从一个换成另外一个。
其中最常见的方式是将接口换成一个测试的实现。你可能没有办法测试DamselRescuingKnight ,但是你可以很容易地测试BraveKnight,通过模拟一个Quest的实现,像下面这样:
import org.junit.Test;
import static org.mockito.Mockito.*;
import org.junit.Test;
/** * Created by Robert.wang on 2016-3-9-0009. */
public class BraveKnightTest {
@Test
public void testEmbarkOnQuest() throws Exception {
Quest quest = mock(Quest.class);
BraveKnight knight = new BraveKnight(quest);
knight.embarkOnQuest();
verify(quest, times(1)).embark();
}
}
这里,你可以使用mock对象框架,即知名的mockito框架去创建一个mock的Quest实现。使用了mock对
象,你可以创建一个BraveKnight的实例,通过构造器注入mock的Quest对象,然后调用BraveKnight的
embarkOnQuest方法,然后通过mockito的verify去验证mock的Quest对象的embark方法是否被调用一
次。
既然你BraveKnight对象可以处理任何你想传递给他的Quest对象,假设你想传递一个杀死恐龙任何,那么
你可以传递一个SlayDragonQuest给他是合适的。
import java.io.PrintStream;
/** * Created by Robert.wang on 2016-3-9-0009. */
public class SlayDragonQuest implements Quest {
private PrintStream stream;
public SlayDragonQuest(PrintStream stream) {
this.stream = stream;
}
@Override
public void embark() {
stream.println("Embarking on quest to slay the dragon");
}
}
就像你所看到的一样,SlayDragonQuest实现了Quest接口,使得它适合BraveKnight。你可能会注意到,
不像一些简单的java程序那样直接使用System.out.println。SlayDragonQuest通过构造器注入一个
PrintStream使得它更加的通用。在这里最大的问题就是,你怎么传递SlayDragonQuest给BraveKnight,
怎么传递PrintStream给SlayDragonQuest。
应用组件之间创建关联的行为通常称为布线或者装配(wiring)。在Spring中,组件之间的装配方式有很多
种,但是一个通常的方式是使用XML。接下来的清单展示了一个简单的Spring配置文件–knights.xml,它将
一个SlayDragonQuest、BraveKnight和一个PrintStream装配起来。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="knight" class="com.springinaction.knights.BraveKnight">
<!--quest注入quest的Bean-->
<constructor-arg ref="quest"/>
</bean>
<!--创建Quest-->
<bean id="quest" class="com.springinaction.knights.SlayDragonQuest">
<constructor-arg value="#{$(System).out}"/>
</bean>
</beans>
这里,BraveKnight和SlayDragonQuest被声明为Bean,在BraveKnight Bean中,通过传递一个Quest的
引用作为构造函数的参数。同时,SlayDragonQuest使用Spring表达式语言传递一个System.out的构造函
数参数给SlayDragonQuest对象。如果XML配置文件不适合你的口味,你可以使用Java方式进行配置。如
下:
package com.springinaction.knights.config;
import com.springinaction.knights.BraveKnight;
import com.springinaction.knights.Knight;
import com.springinaction.knights.Quest;
import com.springinaction.knights.SlayDragonQuest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/** * Created by Robert.wang on 2016-3-9-0009. */
@Configuration
public class KnightConfig {
@Bean
public Knight knight() {
return new BraveKnight(quest());
}
@Bean
public Quest quest() {
return new SlayDragonQuest(System.out);
}
}
不管使用xml还是java,依赖注入的好处都是一样的。尽管BraveKnight依赖Quest,但是它不需要知道具体
是什么Quest,同样的,SlayDragonQuest也不需要知道具体的PrintStream类型。在Spring中,仅仅通过
配置使得所有的片段组装在一起。这个就使得可以去改变他们之间的依赖关系而不需要去修改类的实现。
这个例子已经展示了一个简单的Spring装配。现在你不要考虑太多,我们在第二章的时候会更加深入地探
讨,我们可以发现另外一个装配方式,即通过自动发现功能。
现在你已经声明了BraveKnight和SlayDragonQuest之间的关系,接下来就是加载配置文件并且运行应用。
在一个Spring应用程序中,一个应用程序上下文(Application Context)会加载Bean定义并且将他们组装在一起。Spring上下文完全负责创建并组装Bean,并将他们组成应用程序。Spring提供了几种不同的应用上
下文,他们之间的不同就是加载配置文件的方式的不同。
当使用xml方式的时候,使用ClassPathXMLApplicationContext,这个类通过加载在引用程序目录下面的一
个或者几个xml配置文件。下面的main方法加载knights.xml文件并获取Knight的一个对象引用。
@Test
public void main() {
// 加载Spring上下文
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("knights.xml");
// 获取knight的bean
Knight knight = context.getBean(Knight.class);
// 使用Bean方法
knight.embarkOnQuest();
context.close();
}
现在,让我们看看Spring简化Java开发的第二个策略,基于方面编程(AOP)
虽然DI可以使得你的应用程序组件之间是松耦合的,但是AOP可以使得你可以在你应用程序中去捕获Bean的
功能。
AOP通常被定义为分离软件关注点的一种技术。系统通常由一些具有特定功能的组件组成。但是,通常这些
组件也附带一些除了核心功能之外的一些功能。系统服务,如日志记录、事务管理和安全性,通常会在每个
组件中都是需要的。这些系统服务通常被称为横切关注点(cross-cutting concerns),因为他们会在系统中切割多个组件。
通过传递这些横切关注点,你会提供你应用程序的复杂性:
如下图所示,左边的业务对象紧密耦合右边的系统服务。每一个业务对象不仅需要知道他们的日志记录、安全性和事务性,而且还得实现自己的核心功能。
AOP可以模块化这些服务,并且通过声明式的方式应用这些服务,这将导致组件更加具有凝聚力,并且组件专注于自己特定的功能,对可能涉及的系统服务完全不知情。简单来说,就是让POJO始终保持扁平。
它可能有助于帮助对业务方面的思考,包括许多组件的应用程序,如下所示:
它的核心就是业务模块实现业务功能,使用AOP,你可以覆盖这些业务功能。这一层可以通过一种友好的方
式来进行灵活的应用。这是一个强大的概念,因为它可以分离核心功能与系统服务。为了演示如何在Spring
中使用,请看下面的例子。
假设,你需要记录骑士的来及去的服务,如下:
package com.springinaction.knights;
import java.io.PrintStream;
/** * Created by Robert.wang on 2016-3-9-0009. */
public class Minstrel {
private PrintStream stream;
public Minstrel(PrintStream stream) {
this.stream = stream;
}
//called before quest
public void singleBeforeQuest() {
stream.println("Fa la la, the knight is so brave");
}
//called after quest
public void singleAfterQuest() {
stream.println("Tee hee hee, the brave knight did embark on a quest");
}
}
就像你看到的一样,Minstrel是一个包含两个方法的简单对象,这是简单的,将这个注入进我们之前的代
码,如下所示:
package com.springinaction.knights;
/** * Created by Robert.wang on 2016-3-9-0009. */
public class BraveKnight implements Knight {
private Quest quest;
private Minstrel minstrel;
public BraveKnight(Quest quest, Minstrel minstrel) {
this.quest = quest;
this.minstrel = minstrel;
}
public void embarkOnQuest() {
minstrel.singleBeforeQuest();
quest.embark();
minstrel.singleAfterQuest();
}
}
现在,你需要做的就是在Spring的配置文件中加入Ministrel的构造函数参数。但是,等等….
好像看起来不对,这个真的是骑士本身关注的吗?骑士应该不必要做这个工作。毕竟,这是一个歌手的工
作,歌颂骑士的努力,为什么其实一直在提醒歌手呢?
另外,由于骑士必须知道歌手,你被迫传递歌手给骑士,这个不仅使骑士的代码复杂,而且让我很困惑,当
我需要一个骑士而没有一个歌手的时候,如果Ministrel为null,在代码中还得进行非空判断,简单的
BraveKnight代码开始变得复杂。但是使用AOP,你可以宣布歌手必须歌唱骑士的任务,并且,释放骑士,直接处理歌手的方法。
在Spring配置文件中,你需要做的就是将歌手声明为一个切面。如下:
<?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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="knight" class="com.springinaction.knights.BraveKnight">
<!--quest注入quest的Bean-->
<constructor-arg ref="quest"/>
</bean>
<!--创建Quest-->
<bean id="quest" class="com.springinaction.knights.SlayDragonQuest">
<constructor-arg value="#{T(System).out}"/>
</bean>
<!--定义歌手的Bean-->
<bean id="ministrel" class="com.springinaction.knights.Minstrel">
<constructor-arg value="#{T(System).out}"/>
</bean>
<aop:config>
<aop:aspect ref="ministrel">
<!--定义切点-->
<aop:pointcut id="embark" expression="execution(* *.embarkOnQuest(..))"/>
<!--定义前置通知-->
<aop:before pointcut-ref="embark" method="singleBeforeQuest"/>
<!--定义后置通知-->
<aop:after method="singleAfterQuest" pointcut-ref="embark"/>
</aop:aspect>
</aop:config>
</beans>
使用Spring的AOP配置一个Ministrel作为切面,在切面里面,定义一个切点,然后定义前置通知(before
advice)和后置通知(after advice)。在两个例子中,pointcut-ref属性都使用了一个embark的引用,这
个切点是通过pointcut元素定义的,它表明通知应该应用在什么地方,表达式的语法遵循AspectJ的切点表
达式语法。
关于AspectJ语法,不用担心,我们在第四章会深入讨论。现在,你只需要知道,在BraveKnight调用
embarkOnQuest,前后,分别调用Ministrel的singleBeforeQuest和singleAfterQuest就行了。这就是所
有的一切。使用一点xml配置,你就将歌手分配为了一个切面,在第四章,你会看到更多的例子。
首先,Ministrel始终是一个POJO,没有任何说明他是用来作为切面的。作为一个切面是通过Spring配置文
件实现的。其次,也是最重要的,Ministrel可以应用到BraveKnight而不需要BraveKnight直接调用它,实
际上,BraveKnight根本不知道Ministrel的存在。
需要指出的是,你可以使用Spring的魔法使得Ministrel作为一个切面,但是Ministrel必须首先是一个Spring
的Bean,关键的就是你可以使用任何Spring Bean作为切面,而且可以将其注入其他的Bean中。
使用AOP是愉快的。但是Spring AOP可以做更实际的事情。等下你会看到,Spring AOP可以提供服务,如
声明式事务和安全。
但是现在,让我们看看Spring简化Java开发的其他方面。
你是不是曾经写代码的时候感觉之前也写过同样的代码?这是不是已经看到了,我的朋友。这个就是样板代
码,那些你写过无数遍去完成相同的任何或者简单的任务的代码。
不幸的是,有很多地方涉及Java API的一堆样板代码。一个通常的样板代码的示例就是连接JDBC并且去查下数据,如果你使用过JDBC,可能写过跟下面相似的代码:
public Employee getEmployeeById(long id) {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
//选取雇员
stmt = conn.prepareStatement("select id, firstname, lastname, salary from " +
"employee where id=?");
rs = stmt.executeQuery();
Employee employee = null;
if (rs.next()) {
// 从数据里面创建对象
employee = new Employee();
employee.setId(rs.getLong("id"));
employee.setFirstName(rs.getString("firstname"));
employee.setLastName(rs.getString("lastname"));
}
return employee;
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return null;
}
以上的代码实现通过雇员ID查询雇员信息,但是,我们敢打赌,你看的很艰难,那是因为,关键的代码被一
堆JDBC的代码所淹没。首先你必须创建一个连接,然后必须创建一个语句,并且最后查询结果。并且,为了
避免JDBC的各种问题,必须抛出异常,对异常进行检测并处理,最后还得对JDBC连接进行清理。关闭连接、语句和结果集。这些也有可能引发JDBC异常,还得进行处理。
最重要的是,你必须为所有的JDBC操作写相同的代码,一个员工的查询只是一小部分代码,其他大部分是JDBC的操作。JDBC不是唯一的样板代码的例子,有许多其他类似的样板代码,如:JMS、JNDI和其他服务的往往涉及相同的重复代码。
Spring消除样板代码的方法是将其封装在模板中,比如Spring 的JdbcTemplate就将所有的JDBC样板代码封装起来,使用的时候只需要注意业务逻辑即可。
比如使用Spring JdbcTemplate可以将getEmployyeeId写成下面的模式。
public Employee getEmployeeByIdd(long id) {
return jdbcTemplate.queryForObject("select id, firstname, lastname, salary from employee where id=?", new RowMapper<Employee>() {
@Override
public Employee mapRow(ResultSet resultSet, int i) throws SQLException {
Employee employee = new Employee();
employee.setId(resultSet.getLong("id"));
employee.setFirstName(resultSet.getString("firstname"));
employee.setLastName(resultSet.getString("lastname"));
return employee;
}
}, id);
}
可以看出,新版的getEmployeeByIdd是非常简单并且只关注获取雇员的这个业务。
我们已经看到,Spring是怎么通过面向POJO编程、DI、AOP及模板来简化Java开发的。同时,向你展示了在XML中怎么配置Bean以及怎么配置切面,但是这些文件是如何被加载的呢?他们又装了什么?让我们看看Spring的容器,这里是你的应用程序的Bean存放的地方。