《Spring in Action》第四版第一章《将 Spring 付诸实践》读书笔记(一)

字数4,614 大约花费18分钟

目录

  1. 1. Spring 的宗旨和关键策略
  2. 2. 释放 POJO 的能量
  3. 3. 依赖注入
    1. 3.1. 使用依赖注入进行单元测试
    2. 3.2. 装配
    3. 3.3. Spring 容器
    4. 3.4. 使用应用上下文
    5. 3.5. bean 的生命周期
  4. 4. 面向方面编程 (AOP)
  5. 5. 使用模板减少冗余代码

Spring 的宗旨和关键策略

Spring in Action(Spring 实战)的这一章是对 Spring 进行概述,讲述了 Spring 的项目宗旨,基本原理和关键策略。并且对 Spring 框架进行了概览,比较 Spring4 与之前版本的区别。这一章的读书笔记分为两篇,这一篇是对 Spring 的项目宗旨,基本原理和关键策略的读书笔记, 下一篇 对 Spring 框架进行概览,并比较 Spring4 与之前版本的区别。

Spring 项目的宗旨是简化 Java 开发,主要使用以下四个关键策略:

  • 使用 POJO 进行轻量化和最小侵入的开发
  • 利用依赖注入和面向接口编程,实现松耦合
  • 通过面向方面编程以及约定来实现声明式编程
  • 通过面向方面编程和模板(template)来消除样板化的重复代码(boilerplate code)

笔者按照 POJO、依赖注入、面向方面编程和使用模板四个方面整理了以下笔记。

释放 POJO 的能量

POJO,全称是 plain old Java object,根据 维基百科,这个概念是在 2000 年提出的,用来给简单的对象命名。什么是简单呢,就是不使用任何对象模型(Java object models)、约定(conventions)和框架。理想情况下,一个 POJO 不能被任何定义限制,它不能被规定要继承特定类,比如:

1
public class Foo extends javax.servlet.http.HttpServlet { ...

不能被规定要实现特定接口,比如:

1
public class Bar implements javax.ejb.EntityBean { ...

不能被规定要加上特定标注,比如:

1
@javax.persistence.Entity public class Baz { ...

而下面这样简单的类才是一个 POJO:

1
2
3
4
5
public class HelloWorldBean {
public String sayHello() {
return "Hello World";
}
}

然而,由于技术限制,有时候必须使用标注实现某些功能,这时,只要在加标注之前是一个 POJO,去掉标注之后仍然是一个 POJO, 那么这个对象也可以当作是一个 POJO。

一个 Spring 组件(component)可以是任何类型的 POJO,Spring 尽可能避免其 API 污染你的程序代码,Spring 既不强迫你的代码实现一个 Spring 定义的接口,也不强迫你的代码继承 Spring 定义的类。实际上,从你的代码本身看不出是在使用 Spring 框架。最坏的情况下,就是要在你的代码里一个 Spring 的标注(anotation),但你的代码仍然是一个 POJO。

POJO 形式简单,但 Spring 通过依赖注入将各个 POJO 组装起来,释放出强大的功能。

依赖注入

Spring 做了很多事,但是 Spring 最重要的是以下两个特性:

  • 依赖注入(dependency injection),简称 DI
  • 面向方面编程(aspect-oriented programming),简称 AOP
    这两个特性有着共同的编程思想,就是每个模块或类都专注于自己做的事,尽量做到松耦合,高内聚,尽可能可重用。依赖注入是在实例管理上做到这一点,而面向方面编程是在业务逻辑上做到这一点。

依赖注入这个词,听起来吓人,让人想到复杂的编程技术和设计模式。但其实并不复杂,使用依赖注入后,代码会明显变得简单、易读、容易测试。

依赖注入在处理类与类之间的引用上发挥作用。什么是依赖?类 A 要使用类 B,我们就说类 A 依赖类 B,从编程语言的角度,其实就是引用类 B 的实例。注入的意思就是,在类 A 外生成类 B 的实例,类 A 直接引用该实例,把主被动态一改,就是将类 B 的实例注入到类 A 中。如果不使用依赖注入,类 A 需要自己维护对 B 的直接引用(必须是具体的类型而不能是抽象类或接口),也就是要自己初始化一个 B 的实例。这导致了高度耦合,难以测试。

下面是一个没有使用依赖注入的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 专门拯救少女的骑士
public class DamselRescuingKnight implements Knight {

private RescueDamselQuest quest; // 拯救少女的远征,quest 是远征的意思

// 与 RescueDamselQuest 高度耦合
public DamselRescuingKnight() {
this.quest = new RescueDamselQuest();
}

// 执行远征任务
public void embarkOnQuest() {
quest.embark();
}
}

这个例子中,RescueDamselQuest 是 DamselRescuingKnight 自己创建的实例。这里有两点需要注意,第一点,RescueDamselQuest 是 DamselRescuingKnight 自己创建的,而不是其它类传过来的,这样限制了 quest 的灵活性。第二点,RescueDamselQuest 是一个具体类型(而不是抽象类或接口),将”远征“的目的限制在”拯救少女”,与 RescueDamselQuest 高度(过度)耦合了。因为骑士远征,可能是为了其它目的,比如屠龙、参加圆桌会议等,而由于过度耦合,embarkOnQuest 没有办法执行这些任务了。

耦合是把双刃剑,过度耦合导致难以测试,难以复用,难以理解,修改 bug 容易引起其它 bug。另一方面,没有耦合,类对要使用的类一无所知,就无法使用,几乎什么都做不了。所以,耦合是必要的,但要仔细管理。
而使用依赖注入可以很好解决这个问题。第三方工具统一生成和管理实例,以及实例之间的依赖关系。对象本身不需要创建或者获取它们使用的实例。

下面的 BraveKnight 与 DamselRescuingKnight 相比,就更加通用。

1
2
3
4
5
6
7
8
9
public class BraveKnight implements Knight {
private Quest quest;
public BraveKnight(Quest quest) {
this.quest = quest;
}
public void embarkOnQuest() {
quest.embark();
}
}

与 DamselRescuingKnight 相比,BraveKnight 有两个改进的地方。第一,Quest 作为参数从其它类传进来,而不是 BraveKnight 自己创建。第二,quest(出征)的类型为接口,而不是具体的 RescueDamselQuest,这样出征的任务就没有被限制。

使用依赖注入进行单元测试

1
2
3
4
5
6
7
8
9
10
11
12
package com.springinaction.knights;
import static org.mockito.Mockito.*;
import org.junit.Test;
public class BraveKnightTest {
@Test
public void knightShouldEmbarkOnQuest() {
Quest mockQuest = mock(Quest.class);
BraveKnight knight = new BraveKnight(mockQuest);
knight.embarkOnQuest();
verify(mockQuest, times(1)).embark();
}
}

借助于 mockito 和 junit,可以很方便地进行单元测试。首先,利用 mockito 的 mock 方法,模拟一个 Quest 对象。再将这个对象的引用注入到 BraveKnight 对象中。执行 embarkOnQuest 方法后,验证是否正好执行一次。从这段代码可以看出,对于 BraveKnight 而言,Quest 是从外部生成,再注入到 BraveKnight 的。

装配

将应用组件关联起来的动作叫做装配(wiring)。可以使用 XML 的方式进行配置,也可以使用 Java 代码。(个人觉得 Java 的方式更加可读和可维护,这里只记录 java 方式。)

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class KnightConfig {
@Bean
public Knight knight() {
return new BraveKnight(quest());
}
@Bean
public Quest quest() {
return new SlayDragonQuest(System.out);
}
}

这个配置文件为 Knight 和 Quest 接口创建了实例,并且将 Quest 的实例注入到 Knight 中。也可以说是把 Quest 装配到 Knight 中(Quest 还可以继续装配到其它 bean 里)。但是它们是如何加载,又加载到哪里呢?答案是,应用程序的 bean 是在 Spring 容器中。将这两个 bean 注册到应用上下文后,通过应用上下文可以获取到这些 bean。

Spring 容器

在使用 Spring 的应用中,所有的对象都存活于 Spring 容器中。容器创建对象,配置它们,将它们装配在一起,并管理它们从 new 到 finalize 的完整生命周期。

Spring 容器
Spring 容器是 Spring 框架的核心,Spring 容器使用框架来管理应用程序的组件,包括创建组件直接的关联。不存在单一的 Spring 容器,Spring 容器是由可分为两类的一些容器构成。bean 工厂(Bean factories,由 org.springframework.beans.factory.BeanFactory 接口定义)是最简单的容器,为依赖注入提供基础的支持。应用上下文(Application contexts,由 org.springframework.context.ApplicationContext 接口定义),是一个提供了应用框架层级服务的 bean 工厂。这些服务包括:从属性文件解析文本,将应用的事件发布到相应的事件监听器等。对于大部分应用来说,bean 工厂的级别太低,能做的事太少,所以偏好使用应用上下文。

使用应用上下文

Spring 偏好使用应用上下文,以下是主要的上下文:

  • AnnotationConfigApplicationContext,从一个或多个 Java 配置文件中加载 Spring 应用上下文。
  • AnnotationConfigWebApplicationContext,从一个或多个 Java 配置文件中加载 Spring web 应用上下文。
  • ClassPathXmlApplicationContext,从 classpath 目录下的一个或多个 xml 配置文件中加载上下文定义,并且将上下文定义看作 classpath 的资源。
  • FileSystemXmlApplicationContext,从文件系统下的一个或多个 xml 配置文件中加载上下文定义。
  • XmlWebApplicationContext,从 web 应用程序中的一个或多个 xml 配置文件中加载上下文定义。

AnnotationConfigWebApplicationContext 和 ApplicationContext 涉及 Spring web 的内容将在第八章展开,先看 FileSystemXmlApplicationContext 和 ClassPathXmlApplicationContext 这两个使用 xml 加载上下文的例子。这两种方式和从 bean 工厂加载 bean 相似。

1
2
// 使用 FileSystemXmlApplicationContext 加载 applicationContext
ApplicationContext context = new FileSystemXmlApplicationContext("c:/knight.xml");
1
2
// 使用 ClassPathXmlApplicationContext 加载 applicationContext
ApplicationContext context = new ClassPathXmlApplicationContext("knight.xml");

两种方式的区别在于,FileSystemXmlApplicationContext 在文件系统里的特定位置查找 xml 配置文件,而 ClassPathXmlApplicationContext 在 classpathl 里(包括 jar 里)的任何地方查找 xml 配置文件。

还可以使用 java 配置的方式,用 AnnotationConfigApplicationContext 加载上下文。

1
2
// 使用 AnnotationConfigApplicationContext 加载 applicationContext
ApplicationContext context = new AnnotationConfigApplicationContext(com.springinaction.knights.config.KnightConfig.class);

获取到 context 之后,可以使用 context 的 getBean()方法获取 bean。

1
2
3
4
5
6
7
8
9
10
public class KnightMain {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context =
 new ClassPathXmlApplicationContext(
"META-INF/spring/knight.xml");
Knight knight = context.getBean(Knight.class);
knight.embarkOnQuest();
context.close();
}
}

bean 的生命周期

在传统的 java 应用中,bean 的生命周期很简单,用 new 初始化,不用的时候,垃圾回收机制会自动回收。
Spring 的 bean 的生命周期
生命周期里的每一步,都可以根据需要进行定制。下面分别解释这些步骤。

  1. Spring 初始化 bean。
  2. Spring 将值和 bean 的引用注入到 bean 的属性里。
  3. 如果 bean 实现了 BeanNameAware 接口,Spring 把 bean 的 ID 传给 setBeanName()方法。
  4. 如果 bean 实现了 BeanFactoryAware 接口,Spring 会调用 setBeanFactory 方法,将 bean factory 传入。
  5. 如果 bean 实现了 ApplicationContextAware 接口,Spring 会调用 setApplicationContext()方法,将 bean 的引用传给封闭的应用上下文(application context)。
  6. 如果 bean 实现了 BeanPostProcessor 接口,Spring 会调用这个接口的 postProcessBeforeInitialization()方法。
  7. 如果 bean 实现了 InitializingBean 接口,Spring 会调用这个接口的 afterPropertiesSet()方法。类似地,如果这个 bean 使用了初始化方法进行声明, 特定的初始化方法也会被调用。
  8. 如果 bean 实现了 BeanPostProcessor 接口,Spring 会调用这个接口的 postProcessAfterInitialization()方法。
  9. 此时,bean 已经在应用上下文(application context)中,可以被应用使用了。当应用上下文销毁时,bean 才会被销毁。
  10. 如果 bean 实现了 DisposableBean 接口,Spring 会调用这个接口的 destroy 方法,同时,如果 bean 使用了销毁方法进行声明,特定的销毁方法会被调用。

(笔者再说说平时编程时感受到的,依赖注入的一些好处。由于 Spring 容器将所有的实例统一管理,类 A 在使用类 B 时,只需要声明对类 B 的引用,就可以直接从 Spring 容器里获取 B 的实例,不用再重新生成一个 B 的实例。假定实例范围定义为全局唯一,如果 A2 也使用类 B,也只需要声明对类 B 的引用,获取同一个实例,依赖注入可以减少重复实例化,避免重复生成实例的开销。依赖注入更大的好处体现在引用具有传递性时,比如类 A 使用类 B,而类 B 又使用类 C。因为从功能上看,类其实是方法的集合,类 A 可能只使用类 B 的一个方法,而这个方法并未使用类 C。那么实际上在业务和功能上,类 A 只需要和类 B 耦合,根本不使用类 C。如果没有使用依赖注入,如果类 A 要使用类 B,需要显式地实例化 B,而要实例化 B,又要先实例化 C,特别是在 C 的构造函数带有参数时,过程令人抓狂。由于各个类之间的引用情况可能非常复杂,引用链可能很长,这个过程会非常痛苦。)

面向方面编程 (AOP)

依赖注入实现了组件的松耦合,而面向方面编程可以使用全系统范围的可重用的组件的功能。

面向方面编程将系统的关注点分离。系统由组件构成,各个组件负责自己的功能。但是有时候,组件也要处理一些非核心功能。像日志、事务管理和安全性这样的功能在各个组件中经常出现,而这些组件的核心功能并不是这些功能。

在多个组件里都关注这些非核心功能(的实现),既使得这些功能的代码重复,又使组件的代码被这些代码污染,显得杂乱。

不使用 aop

而面向方面编程可以让组件只关心核心功能,实现高内聚,并且确保 POJO 保持朴素(plain)。

使用 aop

日志、事务和安全等各个方面(aspect)就像一张张毯子,覆盖在各个组件上,毯子只关心自己的业务,不侵入到组件内部,组件也不用关心毯子的存在。

下面的例子是没有使用 AOP 时,骑士要自己负责在行动前和行动后,提醒歌手唱歌。

1
2
3
4
5
6
7
8
9
10
11
12
13
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() throws QuestException {
minstrel.singBeforeQuest();
quest.embark();
minstrel.singAfterQuest();
}
}

这样会有一些问题,从业务角度,歌手应该要自己观察骑士的行为,自动唱歌,而不应该是骑士提醒。而且骑士根本不需要知道歌手的存在。从代码角度来说,骑士只管行动就好了,处理唱歌不仅让代码显得杂乱,还要注入 minstrel 对象,这也导致要检查这个对象是否为 null。所以需要将 Minstrel 配置成方面(aspect),并且进行其它配置,让 Minstrel 观察骑士的行为,并在适当的时间做适当的事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?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/aop
http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="knight" class="com.springinaction.knights.BraveKnight">
<constructor-arg ref="quest" />
</bean>
<bean id="quest" class="com.springinaction.knights.SlayDragonQuest"> <constructor-arg value="#{T(System).out}" />
</bean>
<bean id="minstrel" class="com.springinaction.knights.Minstrel">
<constructor-arg value="#{T(System).out}" />
</bean>
<aop:config>
<aop:aspect ref="minstrel">
<aop:pointcut id="embark"
expression="execution(* *.embarkOnQuest(..))"/>

 <aop:before pointcut-ref="embark"
method="singBeforeQuest"/>

<aop:after pointcut-ref="embark"
method="singAfterQuest"/>

</aop:aspect>
</aop:config>
</beans>

bean 标签定义了各个实例,aop:config 标签定义了 aop 的配置,详细内容会在第四章讲解。

使用模板减少冗余代码

有时候为了相似的功能,一遍又一遍地写相同的代码,这些代码就是样板化的重复代码(boilerplate code)。Java 的 API 中就有很多这样的代码,比如 JDBC。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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=?");
stmt.setLong(1, 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"));
employee.setSalary(rs.getBigDecimal("salary"));
}
return employee;
} catch (SQLException e) {
} finally {
if(rs != null) {
try {
rs.close();
} catch(SQLException e) {}
}
if(stmt != null) {
try {
stmt.close();
} catch(SQLException e) {}
}
if(conn != null) {
try {
conn.close();
} catch(SQLException e) {}
}
}
return null;
}

查询的代码被埋没在 JDBC 规定的一堆代码里。首先,创建连接,然后创建 statement,再查询以获得,还要处理这些过程产生的异常。在这些都做完了之后,还要进行清理,包括关闭结果集,关闭 statement,关闭连接,当然,也还要处理这个过程产生的异常。大部分代码都和查询无关,而是来自 JDBC 的样板化的重复代码(boilerplate code)。不仅是 JDBC,JMS、JNDI 以及 REST 服务的使用方的代码,也经常会有重复代码。Spring 通过将这些代码包装到 template 里,来减少样板化的代码。在 JDBC 方面,Spring 使用 JdbcTemplate 做到这一点。下面是 Spring 使用 SimpleJdbcTemplate 的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Employee getEmployeeById(long id) {
return jdbcTemplate.queryForObject(
"select id, firstname, lastname, salary" +
"from employee where id=?",
new RowMapper<Employee>() {
public Employee mapRow(ResultSet rs,
int rowNum) throws SQLException
{

Employee employee = new Employee();
employee.setId(rs.getLong("id"));
employee.setFirstName(rs.getString("firstname"));
employee.setLastName(rs.getString("lastname"));
employee.setSalary(rs.getBigDecimal("salary"));
return employee;
}
},
id);
}

这段代码将 JDBC 的那堆样板化的重复代码都交给 jdbcTemplate 处理,只关注查询任务本身,代码精简,也更加专注。

以上讲解了 Spring 利用面向 POJO 开发、依赖注入、面向方面编程和模板,来减少 Java 开发中代码的复杂性。同时,也展示了如何配置 bean 以及如何使用 XML 配置一个方面(aspect)。特别是对依赖注入和面向方面编程有了初步的了解。但是很多内容还是留有疑问,比如装配(wiring)方面的细节,将在第二章和第三章详述,而面向方面编程将在第四章展开。

谈谈 IT的文章均为原创或翻译(翻译会注明外文来源),转载请以链接形式标明本文地址: http://tantanit.com/springinaction-di-si-ban-di-yi-zhang-jiang-spring-fu-zhu-shi-jian-du-shu-bi-ji-yi/

谈谈IT

欢迎关注官方微信公众号获取最新原创文章