基于《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 | // Spring doesn’t make any unreasonable demands on HelloWorldBean |
从上面例子可以看到,这就是一个简单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 | package com.springinaction.knights; |
上述代码,存在两方面的问题:
- 紧耦合、难以扩展:两个类
DamselRescuingKnight
与RescueDamselQuest
紧密联系在一起。如果要一个屠龙的knight,则上述代码都得重新写,很难直接利用。 - 测试测试困难:单元测试需要知道是否正确调用了
quest.embark()
,目前没有简洁的方法可以进行验证。
依赖注入(DI)
使用DI,则有一个第三方来负责协同两个类。看下面例子(constructor injection):
1 | package com.springinaction.knights; |
上面的BraveKnight
就比较通用,可以接收各种Quest
:RescueDamselQuest
, SlayDragonQuest
, MakeRoundTableRounderQuest
。
因此BraveKnight
已经与Quest
的各种具体实现完全解耦了,不再依赖于具体实现。 (设计模式原则之一:基于接口编程。策略模式)
采用constructor injection后,很容易使用Mock来进行单元测试:
1 | package com.springinaction.knights; |
通过采用调用embarkOnQuest()
,可以利用Mockito
来验证Quest
的embark()
是被调用了1次。
注入Quest
首先实现被注入的类SlayDragonQuest
:
1 | package com.springinaction.knights; |
可以看到,SlayDragonQuest
也使用了构造注入(constructor injection)。
在Spring中,可以使用XML来把SlayDragonQuest
传递给BraveKnight
:
1 | <?xml version="1.0" encoding="UTF-8"?> |
在上述XML文件中,两个类都被声明为Bean了。每个Bean都配置了构造函数输入参数,从而关联的对应的类(wire beans)。
在Spring中,可以用Java代码来等价替换上述XML文件:
1 | package com.springinaction.knights.config; |
工作起来
在Spring application中,一个application context
来读取bean定义,并把他们wire在一起。
Spring提供了application context的多种实现,其差别仅仅在于如何load configuration。 本处以XML文件为例,需要使用ClassPathXmlApplicationContext
(Java配置文件时,需要使用AnnotationConfigApplicationContext
):
1 | package com.springinaction.knights; |
AOP
DI实现了模块间的解耦(松耦合),AOP可以帮组我们提高功能模块的可重用性。
系统的不同功能分为不同的模块。但在实际过程中,一些模块除了自己的核心功能以外,还有一些其他的辅助功能,而这些辅助的功能可能会涉及多个模块。这就造成,修改一些代码,可能会涉及到系统很多部分。
定义一个类Minstrel
,其有两个函数,分别在knight做一个quest(embark on a quest)之前和之后执行:
1 | package com.springinaction.knights; |
传统处理方式
直接在某个具体骑士的类中调用minstrel
的两个函数:
1 | package com.springinaction.knights; public class BraveKnight implements Knight { private Quest quest; private Minstrel minstrel; public BraveKnight(Quest quest, Minstrel minstrel) { |
在这个例子中,两个类紧紧耦合在一起了。
AOP处理方式
在Spring中,AOP处理方式对应的XML配置如下:
1 | <?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"> |
在上述配置中,首先把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); } |