Spring in Action——Spring之旅
根本使命
Spring的目标是致力于全方位的简化java开发:
- 基于POJO的轻量级和最小侵入性编程
- 通过依赖注入和面向接口实现松耦合
- 基于切面和惯例进行声明式编程
- 通过切面和模版减少样板式代码
POJO
Spring竭力避免因为自身API而弄乱你的应用代码。Spring不会强迫你实现Spring规范的接口或继承Spring规范的类,相反,在基于Spring构建的应用中,它的类通常没有任何痕迹表明你使用了Spring。
依赖注入DI
public class DamseRescuingKnight implements Knight{
private RescueDamseQuest quest;
public DamseRescuingKnight(){
this.quest=new RescueDamseQuest;
}
public void embarkOnQuest(){
quest.embark();
}
}
可以看到,它的构造函数自己创建了一个RescueDamseQuest,这使得耦合度太高。并且使得测试异常困难,在这样也测试中,你必须保证当骑士的embarkOnQuest()方法被调用的时候,探险的embark()方法也要被调用,但是没有一个简单明了的方式能够实现这一点。很遗憾,DamseRescuingKnight将无法进行测试。
那么实现了DI之后,如下:
public class BraveKnight implements Knight{
private Quest quest;//变成了Quest,而不是它的实现(比如RescueDamseQuest或者SlayDragonQuest)
public BraveKnight(Quest quest){//构造方法注入
this.quest=quest;
}
public void embarkOnQuest(){
quest.embark();
}
}
这里的要点是BraveKnight 没有和 任何 特定的 Quest发生耦合,对他来说,只要是实现了Quest接口的探险任务就可以,具体是哪种无关紧要。
对依赖进行替换的一个常用方法就是在测试的时候使用mock实现,在使用DI后,我们可以轻松的测试BraveKnight:
public class BraveKnightTest{
@Test
public void knightShouldEmbarkOnQuest(){
Quest mockQuest=mock(Quest.class);
BraveKnight knight=new BraveKnight(mockQuest);//注入mockQuest
knight.embarkOnQuest();
verify(mockQuest,times(1).embark();)
}
}
使用XML装配:
<beans>
<bean id="knight" class="..BraveKnight">
<constructor-arg ref="quest"/>
</bean>
<bean id="quest" class="..SlayDragonQuest"></bean>
</beans>
如果XML配置不符合你的喜好,那么还可以使用java:
@Configuration
public class KnightConfig{
@Bean
public Knight knight(){
return new BraveKnight(quest());
}
@Bean
public Quest quest(){
return new SlayDragonQuest(System.out);
}
}
Spring通过应用上下文(Application Context)装在bean的定义并且把它们组装起来。因为knights.xml中的bean是使用XML文件进行配置的,所以选择ClassPathXmlApplicationContext作为应用上下文相对是比较核实的。该类加载位于应用程序路径下的一个或多个XML配置文件:
public class KnightMain{
public static void main(String[] args)throws Exception{
ClassPathXmlApplicaitionContext context=new ClassPathXmlApplicationContext("META-INF/spring/knights.xml");
Knight knight=context.getBean(Knight.class);//获取Knight bean
knight.embarkOnQuest();//使用kngiht
context.close();
}
}
AOP
AOP使得诸如事务管理、安全模块、日志模块等服务模块化(因为如果不使用AOP,他们到处存在于java代码中)。所以可以使得组件会高内聚,并且更加关注自身的业务,而不需要关注系统服务而带来复杂性。
也就是AOP是的POJO简单。
为了演示Spring中如何应用AOP,我们重新回到骑士的例子:
假设吟游诗人用诗歌记载了骑士的事迹(日志)。
public class Minstrel{
private PrintStream stream;
public Minstrel(PrintStream stream){
this.stream=stream;
}
public void singBeforeQuest(){//探险前调用
stream.println("Fa la la, the knight is so brave");}
public void SingAfterQuest(){//探险后调用
stream.println("Tee hee hee, the brave knight"+
"did embark on a quest !");}
}
Minstrel会通过一个PrintStream类来歌颂骑士的事迹,分别在探险前后都会歌颂。下面的程序尝试了吟游诗人和骑士的第一次组合:
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();
}
}
Knight应该管理它的Minstrel么?显然不是,它脱离了Knight的职责,此外骑士需要知道吟游诗人,所以必须把它注入到自己的类中,会使得自己更加复杂。
要将Minstrel抽象为一个切面,所需要做的事情就是在一个Spring的配置文件里声明它:
<bean id="knight" class="..BraveKnight">
<constructor-arg ref="quest"/>
</bean>
<bean id="quest" class="..SlayDragonQuest">
<constructor-arg value="#(T(System).out)"/>
</bean>
<!--声明Minstrel bean-->
<bean id="minstrel" class="..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>
这里使用了aop配置命名空间把Minstrel bean声明为一个切面。首先,需要把Minstrel声明为一个bean,然后在<aop:aspect>
元素中引用该bean。为了进一步定义切面,声明(使用<aop:before>
)在embarkOnQuest()放啊执行前调用Minstrel的singBeforeQuest()方法,这种方式称为前置通知。同时也有后置通知。
这两种方式中,pointcut-ref属性都引用了名字为embark的切入点,该切入点实在前面的<pointcut>
元素中定义的,并配置expression属性来选择所应用的通知。表达式的语法采用的是AspectJ的切点表达式语言(现在毋须了解AspectJ或者编写AspectJ的切点表达式语言的细节,之后会重复)。
使用模版消除样板式代码
举个例子,如果你曾经使用过jdbc,相信你写过如下类似的代码:
public Employee getEmployeeById(long id){
Connection conn=null;
PreparedStatement stmt=null;
ResultSet rs=null;
try{
conn=dataSource.getConnection();
...
}catch(SQLException e){...}
finally{
if(rs!=null){try{...}catch(...){...}}
...
}
}
正如你所看到,少量的查询代码淹没在一堆JDBC样板式代码中,建立链接关闭链接。JDBC不是产生样板式代码的唯一场景,JMS/JNDI/REST服务等等都会产生大量的重复代码。
Spring旨在通过模版封装来消除样板式代码,举个例子,使用Spring的JdbcTemplate重写getEmployeeById()方法仅仅关注获取员工数据的核心逻辑,而不需要迎合JDBC API的需求:
public Employee getEmployeeById(long id){
return jdbcTemplate.queryForObject{
"select id,firstname,lastname,salary form emplyee where id=?",
new RowMapper<Employee>(){
public Employee mapRow(ResultSet rs,int rowNom)throws SQLExcepion{
Employee employee=new Employee();
employee.setId(rs.getLong("id"));
employee.setFirstName(rs.getString("firstname"));
...//各种set
return employee;
}
},
id);//指定查询参数
}
新版本的getEmployeeById()简单多了,而且仅仅关注于从数据库中查询员工。模版的queryForObject()方法需要一个SQL查询语句,一个RowMapper对象(把数据映射为一个域对象),零个躲着多个查询参数。
容纳你的bean
Spring容器并不是只有一个,Spring自带了多个容器实现,可以归为两种不同的类型:BeanFactory是最简单的容器,提供基本的DI支持;ApplicationContext基于BeanFactory构建,并提供应用框架级别的服务。
但是BeanFactory对于大多数应用来说往往太低级了,我们这里主要讨论ApplicationContext的使用上。
使用ApplicaitonContext
下面罗列几个最有可能遇到的应用上下文:
AnnotationConfigApplicationContext:从一个或者多个基于Java配置类中加载Spring应用上下文。
AnnotationConfigWebApplicationContext:从一个或者多个基于Java配置类中加载Spring Web应用上下文。
XmlWebApplicationContext:从Web应用下的一个或者多个XML配置文件中加载上下文定义。
ClassPathXmlApplicationContext:从类路径下的一个或多个XML配置文件中加载上下文定义,把应用上下文的定义文件作为类资源。
FileSystemXmlApplicationContext:从文件系统下的一个或者多个XML配置文件中加载上下文定义。
我们之后会提到前三种ApplicaitonContxt。我们现在先简单使用ClassPathXmlApplicationContext和FileSystemXmlApplicationContext。无论是从文件系统还是类路径装在应用上下文,将bean加载到BeanFactory的过程是类似的,区别在于一个是从文件系统路径下查找xml文件,另一个是在所有的类路径下查找xml文件。
ApplicationContext context=new FileSystemXmlApplicationContext("C:/knight.xml");
//或者
ApplicationContext context=new ClassPathXmlApplicationContext("knight.xml");
而如果想从java配置中加载应用上下文:
ApplicationContext context=new AnnotationConfigApplicationContext(com.springinaction.knights.config.KnightConfig.class);
bean的生命周期
Spring生态圈
spring模块
当我们下载Spring发布版本并查看其lib目录的时候,会发现里面有多个jar文件。在Spring4.0中,Spring框架的发布版本包括了20多个不同的模块。
这些模块根据功能可划分为6种。
总而言之,这些模块为企业级应用开发提供了所需的一切。
让我们逐一浏览Spring模块,看看它们是怎么构建起Spring生态圈的
Spring核心容器
容器是Spring框架最核心的部分,它掌管着Spring应用中bean的创建、配置和管理。包括了bean工厂——提供DI功能。基于bean工厂,我们还可以知道有Application Factory的存在。
Spring的AOP模块
AOP解耦合。
数据访问与集成
简化模版。
Web与远程调用
MVC模式是一种普遍被接受的构建Web应用的方法,它可以帮助用户将界面逻辑与应用逻辑分离。java从来不缺少MVC框架,SpringMVC我们将在后续学习到。
远程调用功能集成了RMI/Hessian/Burlap/JAX-WS等等,后面我们会学习到。
Instrumentation
Spring的Instrumentation模块提供了为JVM添加代理(agent)的功能。具体的来讲,它为Tomcat提供了一个织入代理,能够为Tomcat传递类文件,就像这些文件是被类加载器加载的一样。在本书中,我们不介绍该模块。
测试
监狱开发者自测的重要性,Spring提供了测试模块。通过该模块,Spring为使用JNDI、Servlet和Porlet编写单元测试提供了一系列mock对象实现。
TDD测试驱动开发。
Spring Portfolio
超出本书范围。暂时跳过。