Spring可以做很多事情,它为企业级开发提供了丰富的功能,这些功能的底层实现都依赖于它的两个核心特性--依赖注入(Dependency Injection, DI)和面向切面编程(Aspect-Oriented Programming, AOP)。
Spring在诞生之初,主要目的是为了替代更加重量级的企业级Java技术,比如EJB等等。相对于EJB来说,Spring提供了更加轻量级和简单的编程模型。它增强了简单老式Java对象(Plain Old Java Object, POJO)的功能,使其具备了之前只有EJB和其他企业级Java规范才具有的功能。
随着时间的推移,EJB以及Java2企业版(Java 2 Enterprise Edition, J2EE)在不断演化,EJB自身也提供了面向简单POJO的编程模型。现在,EJB也采用了依赖注入和面向切面编程的理念,这些无疑是受到了Spring的启发。
尽管J2EE能够赶上Spring的步伐,但是Spring也没有停止前进,它继续在其他很多领域上发展,例如:移动开发、社交API集成、NoSQL数据库、云计算以及大数据,而J2EE则是刚刚开始涉及这些领域,或者还完全没有开始在这些领域的创新。
Spring最根本的使命是简化Java开发。为了降低Java开发的复杂性,Spring采取了以下4种关键策略:
- 基于POJO的轻量级和最小侵入性编程;
- 通过依赖注入和面向接口实现松耦合;
- 基于切面和惯例进行声明式编程;
- 通过切面和模板减少样板代码。
简化Java开发
激发POJO的潜能
很多Java框架强迫你的应用程序继承它们的类或者实现它们的接口,从而导致你的应用和框架绑死。这种侵入式的编程方式在早期版本的Struts、WebWork以及无数其他的Java规范和框架中都能看到。
Spring竭力避免自身的API弄乱你的应用代码,Spring不会强迫你实现Spring规范的接口或继承Spring规范的类,相反,在基于Spring构建的应用中,它的类通常没有任何痕迹表明你使用了Spring。最坏的场景是,一个类或许会使用Spring注解,但它依然是POJO。
看下面这个bean(Spring中用bean或者JavaBean来表示应用组件):
package bean;
public class Robot {
private String name;
private String version;
private Weapon weapon;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public Weapon getWeapon() {
return weapon;
}
public void setWeapon(Weapon weapon) {
this.weapon = weapon;
}
public void attack() {
System.out.println(getName() + " using " + weapon.getName() + " to attack.");
}
public void showDetail() {
System.out.println("i'm robot " + getName() + ", version " + getVersion() + ", i use weapon " + getWeapon().getName() + ".");
}
}
可以看到,这是一个普通的POJO,没有任何地方表明了它是一个Spring组件。Spring的非侵入式编程模型意味着这个类在Spring应用和非Spring应用中都可以发挥同样的作用。
Spring通过DI来装配POJO,DI使得应用对象之间保持松散耦合。
依赖注入
任何一个有实际意义的应用都会由两个或更多个类组成,这些类之间通过相互协作来完成特定的业务逻辑。传统的做法是,每个对象管理与自己相互协作的对象的引用,这样会导致高度耦合,同时难以测试。例如下面这段代码:
package com.springinaction.knights;
public class DamselRescuingKnight implements Knight {
private RescueDamselQuest quest;
public DamselRescuingKnight() {
quest = new RescueDamselQuest();// 与RescueDamselQuest紧耦合
}
@Override
public void embarhOnQuest() throws QuestException {
quest.embark();
}
}
正如你所见,DamselRescuingKnight 在它的构造函数中自行创建了RescueDamselQuest,这使得DamselRescuingKnight和RescueDamselQuest紧密地耦合到了一起,因此极大地限制了这个骑士的执行能力。如果一个少女需要救援,这个骑士能够召之即来。但是如果一条恶龙需要杀掉,那么这个骑士只能爱莫能助了。
通过DI,对象的依赖关系将由系统中负责协调各对象的第三方组件在创建对象的时候进行设定,对象无需自行创建或管理它们的依赖关系,依赖关系将被自动注入到需要它们的对象中去。看下面这段代码:
package com.springinaction.knights;
public class BraveKnight implements Knight {
private Quest quest;
public BraveKnight(Quest quest) {
this.quest = quest;// quest被注入到对象中
}
@Override
public void embarhOnQuest() throws QuestException {
quest.embark();
}
}
不同于之前的DamselRescuingKnight,BraveKnight没有自行创建探险任务,而是在构造器中把探险任务作为参数注入,这也是依赖注入的一种方式,即构造器注入。
更为重要的是,BraveKnight中注入的探险类型是Quest,Quest只是一个探险任务所必须实现的接口。因此,BraveKnight能够响RescueDamselQuest、SlayDraonQuest等任意一种Quest实现,这正是多态的体现。
这里的要点是BraveKnight没有与任何特定的Quest实现发生耦合。对它来说,被要求挑战的探险任务只要实现了Quest接口,那么具体是哪一类型的探险就无关紧要了。这就是依赖注入最大的好处--松耦合。如果一个对象只通过接口(而不是具体实现或初始化的过程)来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。
接下来我们来注入一个Quest到Knight。
创建应用组件之间协作关系的行为称为装配,Spring有多种装配bean的方式,其中最常用的就是通过XML配置文件的方式装配。 例如下面的XML配置,将SlayDragonQuest注入到BraveKnight中。
<?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"></constructor-arg>
</bean>
<bean id="quest" class="com.springinaction.knights.SlayDragonQuest"></bean>
</beans>
这里的SlayDragonQuest只需要实现了Quest接口即可。
除了XML配置,Spring也提供了使用Java来描述配置,具体的各种配置方式以及装配bean的方式后面会详谈。
接下来我们使用bean来完成我们的业务功能。
Spring通过应用上下文ApplicationContext装载bean的定义并把它们组装起来。ApplicationContext全权负责对象的创建和组装。Spring自带了多种应用上下文的实现,它们之间的主要区别仅在于如何加载配置。比如,Spring可以使用ClassPathXmlApplicationContext来加载应用程序类路径下的XML配置文件。代码如下:
package com.springinaction.knights;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class KnightMain {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("knights.xml");// 加载Spring上下文
Knight knight = (Knight) context.getBean("knight");// 获取knight Bean
knight.embarhOnQuest();// 使用knight
}
}
应用切面
DI让相互协作的软件组件保持松散耦合,面向切面编程则允许你把遍布应用各处的功能分离出来,形成可重用的组件。
通常情况下,系统由许多不同组件组成,其中的每一个组件分别负责一块特定功能。除了实现自身核心的功能之外,这些组件还经常承担着额外的职责,诸如日志、事务管理和安全等,此类的系统服务经常融入到有自身核心业务逻辑的组件中去,这些系统服务通常被称为横切关注点,因为它们总是跨越系统的多个组件,如下图所示。
AOP可以使得这些服务模块化,并以声明的方式将它们应用到它们需要影响的组件中去。这样,这些组件就具有更高内聚性以及更加关注自身业务,完全不需要了解可能涉及的系统服务的复杂性。总之,AOP确保POJO保持简单。
如图所示,我们可以把切面想象为覆盖在很多组件之上的一个外壳。利用AOP,你可以使用各种功能层去包裹核心业务层。这些层以声明的方式灵活应用到你的系统中,甚至你的核心应用根本不知道它们的存在。这是一个非常强大的理念,可以将安全、事务和日志关注点与核心业务逻辑相分离。
接下来我们尝试一下AOP的应用。
接上面骑士的故事,现在需要一个吟游诗人来歌颂骑士的勇敢事迹,代码如下:
package com.springinaction.knights;
public class Minstrel {
public void singBeforeQuest() { // 探险之前调用
System.out.println("Fa la la; The knight is so brave!");
}
public void singAfterQuest() { // 探险之后调用
System.out.println("Tee hee he; The brave knight did embark on a quest!");
}
}
如代码中所示,诗人会在骑士每次执行探险前和结束时被调用,完成骑士事迹的歌颂。骑士必须调用诗人的方法完成歌颂:
package com.springinaction.knights;
public class BraveKnight implements Knight {
private Quest quest;
private Minstrel minstrel;
public BraveKnight(Quest quest) {
this.quest = quest;
}
public BraveKnight(Quest quest, Minstrel minstrel) {
this.quest = quest;
this.minstrel = minstrel;
}
@Override
public void embarhOnQuest() throws QuestException {
minstrel.singAfterQuest();
quest.embark();
minstrel.singAfterQuest();
}
}
但是,管理他的吟游诗人是骑士职责范围内的工作吗?简单的BraveKnight类开始变得复杂,如果骑士不需要诗人,那么代码将会更加复杂。
有了AOP,骑士就不再需要自己调用诗人的方法为自己服务了,这就需要把Minstrel声明为一个切面:
<?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">
<constructor-arg ref="quest"></constructor-arg>
</bean>
<bean id="quest" class="com.springinaction.knights.SlayDragonQuest"></bean>
<bean id="minstrel" class="com.springinaction.knights.Minstrel"></bean>
<aop:config>
<aop:aspect ref="minstrel">
<!-- 定义切点 -->
<aop:pointcut expression="execution(* *.embarkOnQuest(..))"
id="embark" />
<!-- 声明前置通知,在切入点之前执行的方法 -->
<aop:before method="singBeforeQuest" pointcut-ref="embark" />
<!-- 声明后置通知,在切入点之后执行的方法 -->
<aop:after method="singAfterQuest" pointcut-ref="embark" />
</aop:aspect>
</aop:config>
</beans>
通过运行结果可以发现,在没有改动BraveKnight的代码的情况下,就完成了Minstrel对其的歌颂,而且BraveKnight并不知道Minstrel的存在。
使用模板消除样板代码
Spring提供了模板封装来消除样板式代码。样板代码的一个典型场景是你使用JDBC操作数据库的时候,你需要加载数据库驱动、取得数据库连接、创建SQL语句、执行SQL语句、关闭结果集、关闭语句、关闭连接。就像下面这样:
public Employee getEmployeeById(long id) {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
stmt = conn.prepareStatement(
"selectid, 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,还有很多场景会让你写出样板式代码,比如JMS、JNDI、使用REST服务等。
Spring旨在通过模板封装来消除样板式代码。例如Spring的JdbcTemplate使得执行数据库操作时,避免传统的JDBC样板代码。
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样板代码则全部被封装到了模板中。
容纳你的bean
在基于Spring的应用中,应用对象生存于Spring容器中。如图所示,Spring容器负责创建对象,装载它们,配置它们并管理它们的整个生命周期,从生存到死亡。
Spring容器并不是只有一个,Spring自带了多个容器实现,可以归为两个类型:
- bean工厂(由org.springframework.beans.factory.BeanFactory接口定义):最简单的容器,提供基本的DI支持;
- 应用上下文(org.springframework.context.ApplicationContext):基于BeanFactory之上构建,提供应用框架级别的服务。
常用的几种应用上下文
- ClassPathXmlApplicationContext:从类路径下的一个或多个XML配置文件中加载上下文;
- FileSystemXmlApplicationContext:从文件系统下的一个或多个XML配置文件中加载上下文;
- XmlWebApplicationContext:从Web应用下的一个或多个XML配置文件中加载上下文;
- AnnotationConfigApplicationContext:从一个或多个基于Java的配置类中加载Spring应用上下文;
- AnnotationConfigWebApplicationContext:从一个或多个基于Java的配置类中加载Spring Web应用上下文。
bean的生命周期
在传统的Java应用中,bean的生命周期很简单,使用new关键字进行bean实例化后,这个bean就可以使用了。一旦该bean不再被使用,则由Java自动进行垃圾回收。
相比之下,Spring容器中bean的生命周期要复杂的多。下图展示了bean装载到Spring应用上下文中的一个典型生命周期过程。
如你所见,在bean准备就绪之前,bean工厂执行了若干启动步骤,详细描述如下:
- Spring对bean进行实例化;
- Spring将值和bean的引用注入进bean对应的属性中;
- 如果bean实现了BeanNameAware接口,Spring将bean的ID传递给setBeanName()接口方法;
- 如果bean实现了BeanFactoryAware接口,Spring将调setBeanFactory()接口方法,将BeanFactory容器实例传入;
- 如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext()接口方法,将bean所在的应用上下文的引用传入;
- 如果bean实现了BeanPostProcessor接口,Spring将调用postProcessBeforeInitialization()接口方法;
- 如果bean实现了InitializingBean接口,Spring将调用afterPropertiesSet()方法。类似地,如果bean使用了init-method声明了初始化方法,该方法也会被调用;
- 如果bean实现了BeanPostProcessor接口,Spring将调用postProcessAfterInitialization()方法;
- 此时此刻,bean已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁;
- 如果bean实现了DisposableBean接口,Spring将调用它的destroy()方法,同样,如果bean中使用了destroy-method声明了销毁方法,也会调用该方法。
Spring模块
Spring框架由6个良好定义的模块分类组成:
你可以自由地选择适合自身应用需求的Spring模块。