前言: Spring的两个核心特性
Spring可以做很多事情, 它为企业级开发提供给了丰富的功能, 但是这些功能的底层都依赖于它的两个核心特性, 也就是依赖注入(dependency injection, DI) 和面向切面编程(aspect-oriented programming, AOP) 。
1. Spring的目标是什么?Spring能实现哪些功能?
在诞生之初, 创建Spring的主要目的是用来替代更加重量级的企业级Java技术, 尤其是EJB。相对于EJB来说, Spring提供了更加轻量级和简单的编程模型。 它增强了简单老式Java对象(Plain Old Java object, POJO) 的功能, 使其具备了之前只有EJB和其他企业级Java规范才具有的功能。
Spring是为了解决企业级应用开发的复杂性而创建的, 使用Spring可以让简单的JavaBean实现之前只有EJB才能完成的事情。
Spring所有的理念都可以追溯到它最根本的使命上: 简化Java开发。
2. 为了降低Java开发的复杂性, Spring采取了以下4种关键策略
> 基于POJO的轻量级和最小侵入性编程;
> 通过依赖注入和面向接口实现松耦合;
> 基于切面和惯例进行声明式编程;
> 通过切面和模板减少样板式代码。
几乎Spring所做的任何事情都可以追溯到上述的一条或多条策略。 Spring通过以上4条理念实现其终极目标:简化Java开发。
这4种策略将在下面进行简单的解释。
3. 如何理解 “基于POJO的轻量级和最小侵入性编程”
【###】什么是侵入性编程?
举个例子,早期很多框架(例如,早期版本的Struts、 WebWork、 Tapestry)通过强迫应用继承它们的类或实现它们的接口从而导致应用与框架绑死。这就是侵入性编程,或者可以说是对象之间的耦合度过高。
而Spring竭力避免因自身的API而弄乱你的应用代码。 Spring不会强迫你实现Spring规范的接口或继承Spring规范的类, 相反, 在基于Spring构建的应用中, 它的类通常没有任何痕迹表明你使用了Spring。 最坏的场景是, 一个类或许会使用Spring注解, 但它依旧是POJO。
Spring的非侵入编程模型意味着这个类在Spring应用和非Spring应用中都可以发挥同样的作用。
4. Spring通过依赖注入(DI)让对象之间保持松耦合,从而实现基于POJO的最小侵入性编程
任何一个有实际意义的应用都会由两个或者更多的类组成, 这些类相互之间进行协作来完成特定的业务逻辑。
【###】不使用 DI 时对象之间的关系如何维护?
按照传统的做法, 每个对象负责管理与自己相互协作的对象(即它所依赖的对象) 的引用, 这将会导致高度耦合和难以测试的代码。
public class Atest implement Test{
private DoAthing doAthing; //DoAthing implemnts DoThing
public Atest(){
this.doAthing = new DoAthing();
}
public void test(){
doAthing.do();
}
}
在上述代码中,Atest类通过构造函数自行创建了DoThings对象,导致Atest类和DoThings类紧密的耦合在一起了。
4.1 耦合具有两面性(two-headed beast)
一方面, 紧密耦合的代码难以测试、 难以复用、 难以理解, 并且典型地表现出“打地鼠”式的bug特性(修复一个bug, 将会出现一个或者更多新的bug) 。 另一方面, 一定程度的耦合又是必须的——完全没有耦合的代码什么也做不了。 为了完成有实际意义的功能, 不同的类必须以适当的方式进行交互。
总而言之, 耦合是必须的, 但应当被小心谨慎地管理。
4.2 依赖注入(DI)
通过DI, 对象的依赖关系将由系统中负责协调各对象的第三方组件在创建对象的时候进行设定。对象无需自行创建或管理它们的依赖关系 。
public class Atest implement Test{
private DoThing doThing; //DoAthing implements DoThing
public Atest(DoThing doThing){
this.doThing = doThing;
}
public void test(){
doThing.do();
}
}
在上述代码中,Atest没有自行创建DoThing对象, 而是在构造的时候把DoThing对象作为构造器参数传入。 这是依赖注入的方式之一, 即构造器注入(constructor injection) 。
Atest没有与任何特定的DoThing实现发生耦合。对它来说只要是实现了DoThing接口的类,那么具体是哪个类(DoAthing、DoBting、DoCthing、...)都无所谓了。
这就是DI所带来的最大收益——松耦合。 如果一个对象只通过接口(而不是具体实现或初始化过程) 来表明依赖关系, 那么这种依赖就能够在对象本身毫不知情的情况下, 用不同的具体实现进行替换。
DI能够让相互协作的软件组件保持松散耦合。
4.3 声明 Atest 和 DoThing 之间的关系
【###】在上述例子中,我们知道Atest的构造函数需要一个DoThing的实现作为参数。但如何将这个参数给它呢?
创建_应用组件(这里可以简单理解为类)之间协作 _ 的行为 _ 通常称为 _ 装配(wiring) 。
Spring有多种装配bean的方式, 采用XML是很常见的一种装配方式。
//test.xml
<bean id="atest" class="com.test.Atest">
<constructor-arg ref="doThing" />
</bean>
<bean id="doThing" class="com.dothing.DoBthing">
Spring还支持使用Java来描述配置。
@Configuration
public class TestConfig{
@Bean
public Test test(){
return new Atest(doThing());
}
@Bean
public DoThing doThing(){
return new DoBthing();
}
}
无论是通过配置文件还是Java配置来进行Bean的装配,DI起的效果是相同的。通过DI,将对象间的依赖关系完全隐藏在配置中,而在代码中是无法看出的。这样的话, 就可以在不改变所依赖的类的情况下, 修改依赖关系。
现在还不需要纠结于Spring装配的细节,只需要知道装配所起的作用是什么,以及上面提到的为什么需要装配Bean。在后面会详细描述Spring装配bean的其他方式, 甚至包括一种让Spring自动发现bean并在这些bean之间建立关联关系的方式(即,使用@Component等注解声明Bean,然后启用自动扫描Bean,接着使用@Autowired等注解注入到依赖类的属性中)。
4.4 装载配置
上面已经声明了 Atest 和 DoThing 的关系, 接下来我们只需要装载XML配置文件, 并把应用启动起来。
Spring通过应用上下文(Application Context) 装载bean的定义并把它们组装起来。 Spring应用上下文全权负责对象的创建和组装。(组装:根据配置文件中对象之间的依赖关系,注入相应的依赖对象)
Spring自带了多种应用上下文的实现, 它们之间主要的区别仅仅在于如何加载配置。
装载xml配置文件使用 ClassPathXmlApplicationContext 上下文实现。
public class TestMain{
public static void mian(String[] args) throws Exception{
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("META-INF/spring/test.xml");
Test test = context.getBean(Test.calss);
test.test();
context.close();
}
}
Spring的 ClassPathXmlApplicationContext 和其他几种上下文实现在下面会再详细描述,这里就不纠结细节了。在这里只需要知道为什么需要装载配置,以及如何装载配置。
5. 基于切面进行声明式编程
【###】面向切面编程的出现是为了解决什么问题?它的目标是什么?
系统往往由许多不同的组件组成, 每一个组件各负责一块特定功能。除了实现自身核心的功能之外, 这些组件还经常承担着额外的职责。诸如日志、 事务管理和安全这样的系统服务经常融入到自身具有核心业务逻辑的组件中去, 这些系统服务通常被称为横切关注点, 因为它们会跨越系统的多个组件。
如果将这些关注点分散到多个组件中去(即上面所说的每个组件还承担着额外的职责), 你的代码将会带来双重的复杂性。
复杂性1: 实现系统关注点功能的代码将会重复出现在多个组件中。 这意味着如果你要改变这些关注点的逻辑, 必须修改各个模块中的相关实现。 即使你把这些关注点抽象为一个独立的模块, 其他模块只是调用它的方法, 但方法的调用还是会重复出现在各个模块中。
复杂性2: 组件会因为那些与自身核心业务无关的代码而变得混乱。例如, 一个向地址簿增加地址条目的方法应该只关注如何添加地址, 而不应该关注它是不是安全的或者是否需要支持事务 。
面向切面编程往往被定义为促使软件系统实现关注点分离的一项技术。 AOP能够使这些服务模块化, 并以声明的方式将它们应用到它们需要影响的组件中去。 所造成的结果就是这些组件会具有更高的内聚性并且会更加关注自身的业务, 完全不需要了解涉及系统服务所带来复杂性。 总之, AOP能够确保POJO的简单性。
【###】我们知道面向对象编程,但什么是面向切面编程,切面又怎么去理解?
我们可以把切面想象为覆盖在很多组件之上的一个外壳。 应用是由那些实现各自业务功能的模块组成的。 借助AOP, 可以使用各种功能层去包裹核心业务层。 这些层以声明的方式灵活地应用到系统中, 你的核心应用甚至根本不知道它们的存在。 这是一个非常强大的理念, 可以将安全、 事务和日志关注点与核心业务逻辑相分离。
【###】它能实现什么功能?
面向切面编程(aspect-oriented programming, AOP) 允许你把遍布应用各处的功能分离出来形成可重用的组件。
5.1 AOP示例
需求:在 Atest 的 test() 方法中调用 DoThing 的 do() 方法,但在调用 do() 方法前后需要进行相应的说明 ---- 在调用 do() 方法前需调用 PrintSomeThing 的 printBefore() 方法,在调用 do() 方法后需调用 PrintSomeThing 的 printAfter() 方法。
【###】示例1:不使用AOP
public class PrintSomeThing{
public void printBefore(){
System.out.print("I will do something");
}
public void printAfter(){
System.out.print("I finish the thing");
}
}
public class Atest implement Test{
private DoThing doThing;
private PrintSomeThing printSomeThing;
public Atest(DoThing doThing,PrintSomeThing printSomeThing){
this.doThing = doThing;
this.printSomeThing = printSomeThing;
}
public void test(){
printSomeThing.printBefore();
doThing.do();
printSomeThing.printAfter();
}
}
上述代码中,Atest 需要在执行 do() 方法前后提醒 PrintSomeThing 调用相应的方法进行说明。但是在 do() 方法执行前后进行说明并不是 Atest 类的职责,而是 PrintSomeThing 类的职责,Atest 类应该只负责做事(执行 do() 方法)。
【###】示例2:使用AOP
先将 PrintSomeThing 声明为一个切面:
//test.xml
<?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="atest" class="com.test.Atest">
<constructor-arg ref="doThing" />
</bean>
<bean id="doThing" class="com.dothing.DoBthing">
<bean id="printSomeThing" class="com.print.PrintSomeThing">
<aop:config>
<aop:aspect ref="printSomeThing">
<aop:pointcut id="test" expression="execution(* *.test(..))"/>
<aop:before pointcut-ref="test" method="printBefore"/>
<aop:after pointcut-ref="test" method="printAfter"/>
</aop:aspect>
</aop:config>
</beans>
【代码说明】这里使用了Spring的aop配置命名空间把 PrintSomeThing bean声明为一个切面。 首先, 需要把 PrintSomeThing 声明为一个bean, 然后在 <aop:aspect> 元素中引用该bean。 为了进一步定义切面, 声明(使用<aop:before>) 在 test() 方法执行前调用 PrintSomeThing 的 printBefore() 方法。 这种方式被称为前置通知(before advice) 。 同时声明(使用<aop:after>)在 test() 方法执行后调用 printAfter() 方法。 这种方式被称为后置通知(after advice) 。
在这两种方式中, pointcut-ref属性都引用了名字为test的切入点。 该切入点是在前边的<pointcut>元素中定义的, 并配置expression属性来选择切入点。 表达式的语法采用的是AspectJ的切点表达式语言。
在这里无需关心AspectJ或编写AspectJ切点表达式或各种通知方式(前置、后置、环绕、...)的细节,在后面会详细讲解。在进行上述配置后,Spring在执行 test() 方法前后就会调用 PrintSomeThing 的 printBefore() 和 printAfter() 方法了。
5.2 示例2相对于示例1实现了两个很重要的点
(1)PrintSomeThing 仍然是一个POJO, 没有任何代码表明它要被作为一个切面使用。但当我们按照上面那样进行配置后, 在Spring的上下文中, PrintSomeThing 实际上已经变成一个切面了。
(2)PrintSomeThing 可以被应用到 Atest 中,而 Atest 不需要显式地调用它。 实际上, Atest 完全不知道 PrintSomeThing 的存在。
6. Spring简化Java开发方式之"使用模板消除样板式代码
【###】什么是样板式代码(boilerplate code)?
通常为了实现通用的和简单的任务, 你不得不一遍遍地重复编写的代码。 它们中的很多是因为使用Java API而导致的样板式代码。
样板式代码的一个常见范例是使用JDBC访问数据库查询数据:
//根据id查询员工信息
public Employee getEmployeeById(long id){
Connection conn = null;
PreparedStatement stmt = null;
ResutlSet rs = null;
try{
conn = dataSource.getConnection();
stmt = conn.prepareStatment("select id,name,sex,age 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.setName(rs.getString("name"));
employee.setSex(rs.getString("sex"));
employee.setAge(rs.getInt("age"));
}
return employee;
}catch(SQLException e){
//doSomeThing ...
}finally{
if(rs != null){
try{
rs.close();
}catch(SQLException e){
//doSomeThing ...
}
}
if(stmt != null){
try{
stmt.close();
}catch(SQLException e){
//doSomeThing ...
}
}
if(conn != null){
try{
conn.close();
}catch(SQLException e){
//doSomeThing ...
}
}
}
return null;
}
首先你需要创建一个数据库连接, 然后再创建一个语句对象, 最后你才能进行查询。 为了平息JDBC可能会出现的怒火, 你必须捕捉SQLException, 这是一个检查型异常, 即使它抛出后你也做不了太多事情。最后,该做的也做了, 你不得不清理战场, 关闭数据库连接、 语句和结果集。 在这里同样为了平息JDBC可能会出现的怒火, 你依然要捕捉SQLException。
而实际上只有少量的代码与真正要做的事情有关系, 其他的代码都是JDBC的样板代码。 且JDBC不是产生样板式代码的唯一场景。 在许多编程场景中往往都会导致类似的样板式代码, JMS、 JNDI和使用REST服务通常也涉及大量的重复代码。
Spring旨在通过模板封装来消除样板式代码。 Spring的JdbcTemplate使得执行数据库操作时, 避免传统的JDBC样板代码成为了可能。举个例子, 使用Spring的JdbcTemplate(利用了 Java 5特性的JdbcTemplate实现) 重写的getEmployeeById()方法仅仅关注于获取员工数据的核心逻辑, 而不需要迎合JDBC API的需求。
public Employee getEmployeeById(long id){
return jdbcTemplate.queryForObject(
"select id,name,sex,age from employee where id=?",
new RowMapper<Employee>(){
public Employee mapRow(ResutlSet rs,int rowNum) throws SQLException{
Employee employee = new Employee();
employee.setId(rs.getLong("id"));
employee.setName(rs.getString("name"));
employee.setSex(rs.getString("sex"));
employee.setAge(rs.getInt("age"));
return employee;
}
},
id
);
}
模板(JdbcTemplate)的 queryForObject() 方法需要一个SQL查询语句, 一个RowMapper对象(把数据映射为一个域对象) , 零个或多个查询参数。 JDBC样板式代码全部被封装到了模板中。
下一篇:Note2