spring最根本的使命,或者说spring被发明出来最根本的想法是:简化Java开发。
针对此,spring做了如下四项工作:
1. 基于POJO的轻量级和最小侵入性编程
2. 通过依赖注入和面向接口实现松耦合
3. 通过切面和惯例进行声明式编程
4. 通过切面和模板减少样板式代码
下面对此进行一一介绍:
POJO(Plain Old Java Object)指的就是简单普通的Java类。Spring在竭力避免自身的api与开发者自己编写的Java类在内容上产生联系,比如说,Spring避免出现如下场景:开发者自己写的类只有继承了Spring的某个类或者实现Spring的某个接口,才能在Spring构建的应用中发挥作用。最坏的情况下,开发者书写的类需要加上Spring的注解,但是,这个类本身依然是POJO。
Spring的非侵入性编程意味着这个类在Spring应用和非Spring应用中都可以发挥相同的作用。
首先,要明确一点,耦合是有两面性的。一定程度的耦合是必须的 —— 完全没有耦合的代码什么也做不了。为了完成具有实际意义的功能,代码之间的耦合是必不可少的。但是,紧密耦合的代码将难以测试、难以复用、难以理解,并且具有典型的“打地鼠”式bug特性(解决了一个bug,将会引发一个甚至多个bug)。
Spring通过DI(依赖注入),使得对象之间的依赖关系由框架中负责协调各对象的第三方组件在创建对象的时候进行设定。对象无需自行创建或者管理自己与其他对象之间的依赖关系。
下面是两个示例:
// 示例一
package com.springinaction.knights;
public class DamselRescuingKnight implements Knight {
private RescueDamselQuest quest;
public DamselRescuingKnight() {
this.quest = new RescueDamselQuest();
}
public void embarkOnQuest() {
quest.embark();
}
}
对于这里的DamselRescuingKnight对象,其与RescueDamselQuest具有非常强的耦合性(创建一个DamselRescuingKnight必须创建一个RescueDamselQuest),这极大地限制了DamselRescuingKnight类的可扩展性。并且,在我们要对这个类进行单元测试的时候,如果我们单纯想测试DamselRescuingKnight的embarkOnQuest()是否正常,我们还必须保证RescueDamselQuest的embark()方法是可以被调用的,即使我们可以去针对RescueDamselQuest去做mock,也凭添了很多复杂的内容。
// 示例二
package com.springinaction.knights;
public class BraveKnight implements Knight {
private Quest quest;
public BraveKnight(Quest quest) { // Quest在此处被注入进来
this.quest = quest;
}
public void embarkOnQuest() {
quest.embark();
}
}
可以看到,对于BraveKnight,其没有自行创建Quest对象,而是在构造的时候,借用参数,将Quest的对象传进来。这便是一种DI的方法,称为构造器注入。
此外,我们可以看到,如果Quest是一个接口,那么其可以有许许多多中的实现,但是我们的BraveKnight没有与任何一种Quest的实现产生耦合。对它来说,只要在构造的时候传入的是Quest的一个实现类的对象,这就足够了,具体是哪种Quest,这个类是并不关心的。
这便是DI所带来的最大收益 —— 松耦合。如果一个对象仅仅通过接口(而不是具体的实现类或初始化过程)来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。
对依赖替换的一个最常用方法就是在测试的时候使用对象的mock。比如我们想测试BraveKnight的时候,只需要提供一个Quest接口的mock实现即可。
AOP:面向切面编程(aspect-oriented programming),允许你把遍布应用各处的功能分离出来形成可重用的组件。
来看示例(使用AOP之前):
// 吟游诗人类
package com.springinaction.knights;
import java.io.PrintStream;
public class Minstrel {
private PrintStream stream;
public Minstrel(PrintStream stream) {
this.stream = stream;
}
public void singBeforeQuest() { // 在骑士出发执行任务前调用
steam.println("Fa la la, the knight is going");
}
public void singAfterQuest() { // 在骑士执行任务结束之后调用
stream.println("Tee hee hee, the knight did embark on a quest");
}
}
// 骑士类(不使用AOP)
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();
minstel.singAfterQuest();
}
}
可以看到,如果想要在BraveKnight进行quest(执行quest.embark())的前后分别执行singBeforeQuest()以及singAfterQuest(),就需要在BraveKnight中注入Minstrel的对象,并且在BraveKnight中调用Minstrel的这两个方法。那么问题来了:
a. 如果现在又有一个骑士类:TestKnight,其也有一个embarkOnQuest()方法,也会在这个方法中执行quest.embark();并且也想在执行前后让吟游诗人唱个小曲助助兴(执行Minstrel那两个方法)。这时候应该怎么办?难道TestKnight也需要注入一个Minstrel的对象,并且在quest.embark();前后执行MinStrel的那两个方法?这样岂不是相同的代码我们不得不写了两遍。如果再有一个类似的骑士:FunKnight,也想让吟游诗人唱两句儿,我们不就需要对相同的代码再写第三遍了吗?显然,这并不是一种良好的代码设计方案。
b. 在骑士类BraveKnight中,为了能够听到吟游诗人唱歌,我们不得不注入了一个Minstrel的对象。换句话说,骑士类知道吟游诗人的存在,并且要告知吟游诗人在什么时候应该唱什么歌。但是,作为一个骑士,真的应该去管理吟游诗人的事情吗?我们希望的场景是,骑士只需要去执行自己的任务(quest.embark()),吟游诗人用诗歌记载骑士的探险事迹,这种事情只需要吟游诗人自己负责就行了,骑士完全没有必要去关心和维护这些事情。
针对这两个问题,Spring允许我们将吟游诗人类抽象为一个切面,然后在配置文件中进行配置,要求将吟游诗人这个切面在骑士执行任务这个切点发挥作用,在执行任务之前执行singBeforeQuest(),执行任务之后执行singAfterQuest()。
还是看示例(使用AOP之后):
// 吟游诗人类(跟上面的一样。。)
package com.springinaction.knights;
import java.io.PrintStream;
public class Minstrel {
private PrintStream stream;
public Minstrel(PrintStream stream) {
this.stream = stream;
}
public void singBeforeQuest() { // 在骑士出发执行任务前调用
steam.println("Fa la la, the knight is going");
}
public void singAfterQuest() { // 在骑士执行任务结束之后调用
stream.println("Tee hee hee, the knight did embark on a quest");
}
}
// 骑士类
package com.springinaction.knights;
public class BraveKnight implements Knight {
private Quest quest;
public BraveKnight(Quest quest) {
this.quest = quest;
}
public void embarkOnQuest() throws QuestException {
quest.embark();
}
}
// Spring中AOP相关配置
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"
>
这样,我们就能够做到,在BraveKnight类中,不用关心Minstrel类,甚至都不需要知道Minstrel类的存在。但是在实际执行的时候,在embarkOnQuest()执行之前,会自动执行singBeforeQuest()方法,而在embarkOnQuest()执行之后,会自动执行singAfterQuest()方法。
此外,对于Minstrel类,尽管我们将其定义为一个切面,但是抛开xml文件,单纯看这个类的时候,我们发现,它仅仅是一个单纯的POJO,没有任何代码表明要将其作为一个切面使用,这也印证了上文中所说的第一点,Spring尽可能地避免自身的api弄乱我们所写的Java类。
还有,我们在xml文件中,将Minstrel声明为切面之前,我们首先创建了一个minstrel的bean,所以,所有其他spring bean可以做到的事情,这里minstrel这个bean都是可以做到的。
- 通过切面和模板减少样板式代码
首先,在第三点的示例中,我们可以看到,如果我们使用了切面,不仅在BraveKnight这个类中,不需要去调用Minstrel的那两个方法,在其他类似的骑士类,比如我们所设想的TestKnight和FunKnight这两个类中也是无需去调用Minstrel的,这就相当于为我们减少了需要书写的代码量。
此外,使用模板能够进一步减小代码量。示例如下:
// 示例一,普通的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 emplyee where id=?"
);
stmt.setLong(1, id);
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 (SQLException e) {
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提供的模板
public Employee getEmployeeById(long id) {
return jdbcTemplate.queryForObject(
"select id, firstname, lastname, salary from employee where id=?",
new RowMapper() {
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.getString("salary"));
return emplyee;
}
},
id
);
}
明显可以看到,使用spring提供的模板,能够减少很多样板式的代码,使得我们更加专注于我们的业务代码,代码也因此具有更高的可读性。