《Spring In Action》学习笔记(一):基本概念

基于《Spring In Action》第四版。

概述

为了替代笨重的Enterprise JavaBeans (EJB),人们设计了轻量级的框架Spring。 因此Spring的设计目的就是:简化java开发。 Spring simplifies Java development。

为了降低Java开发复杂度,Spring采用了四方面的策略

  • Lightweight and minimally invasive development with POJOs
  • Loose coupling through DI and interface orientation
  • Declarative programming through aspects and common conventions
  • Eliminating boilerplate code with aspects and templates

后续四部分会分别针对这四个方面,以实际例子来说明其含义。

先记录一些常用的名词:

  • POJO: plain old Java object (简单Java类)
  • Bean/JavaBean:Spring中使用宽松的定义,就是指一个spring component,当作POJO的同义词
  • DI:Dependency Injection(依赖注入)
  • AOP:Aspect-Oriented Programming

Non-invasive Programming Model

有一些Java框架在使用时,开发人员需要在自己的程序中扩展或者实现框架的的一些interface或者class。

Spring框架不会让程序代码和他的API混在一起。Spring almost never forces you to implement a Sprint-specific interface or extend a Spring-specific class。

通常,使用Spring时,往往感受不到在使用Spring。 最差情况是会使用一些Spring’s annotation。下面是一个例子:

1
2
3
4
5
6
7
8
// Spring doesn’t make any unreasonable demands on HelloWorldBean

package com.habuma.spring;
public class HelloWorldBean {
public String sayHello() {
return "Hello World";
}
}

从上面例子可以看到,这就是一个简单Java类(POJO),没有Spring的组件直接体现出来。这样的好处是:

同样的代码可以在其他非Spring程序中使用(in a non-Spring application)。

Dependency Injection

Dependency Injection (DI) 的用处主要是解耦不同的class/object。

一个典型应用场景是:

  • Class B1来自通用的Inferface B
  • Class A里面需要使用Class B1
    • 若直接在Class A中直接生产Class B1的对象,则两个类型之间产生了紧耦合。不利于后续程序的扩展
    • Class A可以把Interface B的对象作为输入参数,这样就从外界获取Class B1的对象,而不是内部直接产生,从而实现解耦;同时也利于扩展,例如获取Interface B的其他实现B2、B3等。

下面分三部分通过例子来

传统紧耦合方式

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.springinaction.knights;

public class DamselRescuingKnight implements Knight {
private RescueDamselQuest quest;

public DamselRescuingKnight() {
this.quest = new RescueDamselQuest();
}

public void embarkOnQuest() {
quest.embark();
}
}

上述代码,存在两方面的问题:

  1. 紧耦合、难以扩展:两个类DamselRescuingKnightRescueDamselQuest紧密联系在一起。如果要一个屠龙的knight,则上述代码都得重新写,很难直接利用。
  2. 测试测试困难:单元测试需要知道是否正确调用了quest.embark(),目前没有简洁的方法可以进行验证。

依赖注入(DI)

使用DI,则有一个第三方来负责协同两个类。看下面例子(constructor injection):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.springinaction.knights;

public class BraveKnight implements Knight {

private Quest quest;

public BraveKnight(Quest quest) {
this.quest = quest;
}

public void embarkOnQuest() {
quest.embark();
}
}

上面的BraveKnight就比较通用,可以接收各种QuestRescueDamselQuest, SlayDragonQuest, MakeRoundTableRounderQuest

因此BraveKnight已经与Quest的各种具体实现完全解耦了,不再依赖于具体实现。 (设计模式原则之一:基于接口编程。策略模式)

采用constructor injection后,很容易使用Mock来进行单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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();
}
}

通过采用调用embarkOnQuest(),可以利用Mockito来验证Questembark()是被调用了1次。

注入Quest

首先实现被注入的类SlayDragonQuest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.springinaction.knights;
import java.io.PrintStream;

public class SlayDragonQuest implements Quest {
private PrintStream stream;

public SlayDragonQuest(PrintStream stream) {
this.stream = stream;
}

public void embark() {
stream.println("Embarking on quest to slay the dragon!");
}
}

可以看到,SlayDragonQuest也使用了构造注入(constructor injection)。

在Spring中,可以使用XML来把SlayDragonQuest传递给BraveKnight

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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">
<constructor-arg ref="quest" />
</bean>

<bean id="quest" class="com.springinaction.knights.SlayDragonQuest">
<constructor-arg value="#{T(System).out}" />
</bean>
</beans>

在上述XML文件中,两个类都被声明为Bean了。每个Bean都配置了构造函数输入参数,从而关联的对应的类(wire beans)。

在Spring中,可以用Java代码来等价替换上述XML文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.springinaction.knights.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.springinaction.knights.BraveKnight;
import com.springinaction.knights.Knight;
import com.springinaction.knights.Quest;
import com.springinaction.knights.SlayDragonQuest;

@Configuration
public class KnightConfig {

@Bean
public Knight knight() {
return new BraveKnight(quest());
}

@Bean
public Quest quest() {
return new SlayDragonQuest(System.out);
}
}

工作起来

在Spring application中,一个application context来读取bean定义,并把他们wire在一起。

Spring提供了application context的多种实现,其差别仅仅在于如何load configuration。 本处以XML文件为例,需要使用ClassPathXmlApplicationContext(Java配置文件时,需要使用AnnotationConfigApplicationContext):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.springinaction.knights;

import org.springframework.context.support.
ClassPathXmlApplicationContext;

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); // Get knight
bean
knight.embarkOnQuest();
context.close();
}
}

AOP

DI实现了模块间的解耦(松耦合),AOP可以帮组我们提高功能模块的可重用性。

系统的不同功能分为不同的模块。但在实际过程中,一些模块除了自己的核心功能以外,还有一些其他的辅助功能,而这些辅助的功能可能会涉及多个模块。这就造成,修改一些代码,可能会涉及到系统很多部分。

定义一个类Minstrel,其有两个函数,分别在knight做一个quest(embark on a quest)之前和之后执行:

1
2
3
4
package com.springinaction.knights;
import java.io.PrintStream;
public class Minstrel { private PrintStream stream; public Minstrel(PrintStream stream) { this.stream = stream; } public void singBeforeQuest() { // Called before quest stream.println("Fa la la, the knight is so brave!"); }
public void singAfterQuest() { // Called after quest stream.println("Tee hee hee, the brave knight " + "did embark on a quest!"); } }

传统处理方式

直接在某个具体骑士的类中调用minstrel的两个函数:

1
2
package com.springinaction.knights;

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(); } }

在这个例子中,两个类紧紧耦合在一起了。

AOP处理方式

在Spring中,AOP处理方式对应的XML配置如下:

1
2
3
4
5
<?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>

在上述配置中,首先把Minstrel声明为bean,然后通过<aop:aspect>来配置AOP。在这儿,Minstrel仍然是一个POJO。

利用模板消除冗余代码

Eliminating boilerplate code with templates

传统处理方式

JDBC相关程序,会产生很多例行(冗余)代码,很多代码不是程序的核心功能,而是辅助性的代码。

1
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 {                  // Clean up mess
		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;
}

模板处理方式

使用Spring中的模板SimpleJdbcTemplate,只要写核心功能相关的代码,和JDBC API相关的一些辅助性代码都交由模板处理,不需要额外写代码。

1
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);
}