1.Spring实践之旅
Spring创建为了作为重量级企业java技术的替代品(EJB),Spring提供更轻量级的编程模型。
1.1简化java开发
Spring为了解决企业级应用开发的复杂而创建,使用Spring可以让简单的JavaBean实现只有EJB才能完成的事。
Spring用Bean或JavaBean来表示应用组件,但Spring组件不必遵循JavaBean规范。Spring组件可以是任何形式的POJO,本系列内容的JavaBean与POJO是同意词。
Spring最根本的使命:简化Java开发,是全方位的简化。
为了降低复杂度的策略:
(1)基于POJO的轻量级和最小侵入性编程;
(2)通过依赖注入和面向接口实现松耦合;
(3)基于切面和惯例进行声明式编程;
(4)通过切面和模版减少样板式代码。
Spring做的所有事都可依据上述一或多条策略。
1.1.1激发POJO的潜能
很多框架强迫你在使用时继承它们提供的类或实现它们提供的接口----->>>导致应用与框架绑死。EJB2就是这种入侵式编程方式。
Spring不会强迫你继承类,实现接口。在基于Spring搭建的应用中,看不到你使用Spring的痕迹。
最坏的场景:一个类使用Spring的注解,但它依旧是POJO。
举例HelloWorldBean类

Spring没有对该类有任何不合理要求,这是一个简单的POJO,完全没体现它是一个Spring组件。Spring的非入侵编程模型意味着这个类在Spring应用与非Spring应用都可发挥相同作用。
虽简单,但是.......
1.1.2依赖注入DI
使用DI,代码异常简单,更容易理解和调试。
DI如何实现?
所有应用都有2个以上的类,相互协作完成业务逻辑。
传统做法:每个对象负责管理与自己协作的对象(所依赖的对象)的引用---->>导致高耦合,难测试。
比如骑士类Knight,拯救公主的骑士类DamselRescuingKnight实现了Knight,和探险任务类RescueDamselQuest,

DamselRescuingKnight 在构造器自行创建了RescueDamselQuest ,使DamselRescuingKnight与RescueDamselQuest紧耦合,极大限制了这个骑士的探险能力。如果公主需要拯救,这个骑士类可以;如果需要打败恶龙或者让圆桌转动等别的任务,他就不行了。
且DamselRescuingKnight很难单元测试,必须保证DamselRescuingKnight类的embarkOnQuest()方法被调用时,RescueDamselQuest的embark()也被调用。无法简单的证明,所以DamselRescuingKnight类无法测试。
耦合的两面性:
(1)紧耦合难测试,难复用,难理解,打地鼠式bug,修复一个又出一个或更多;
(2)一定程度的耦合是必要的,零耦合代码什么都不能做。为了实现有用的功能,不同类间必须适当交互。
总之,耦合必要,但要谨慎管理。
通过Di,对象的依赖关系将由系统中第三方组件在创建对象时进行设定,该组件专门负责协调各对象的依赖关系。对象不必自己创建或管理它们的依赖关系,DI将依赖关系自动注入到需要它们的对象中去。

Di会将依赖关系自动交给目标对象bar与baz,不用foo自己去获取。
例如,

BraveKnight ,可以完成任何探险任务,不限于解救公主。BraveKnight没有自己创建探险任务,而在构造时把探险任务作构造器参数传入。这是DI的方式之一:构造器注入(constructor injection)。
更重要的,参数为Quest---->>所有探险任务都要实现的接口,所以BraveKnight可以实现任意Quest,如救公主RescueDamselQuest,打恶龙SlayDragonQuest ,转圆桌
MakeRoundTableRounderQuest 等。
要点:BraveKnight没和任何特定的Quest发生耦合,对它来说只要实现Quest接口,什么类型无所谓。---------->>>DI的最大收益,松耦合。
如果一个对象只通过接口表明依赖关系,不用指出具体实现,不用初始化,那这种依赖关系就可以在对象本身无感知的情况下,用不同的具体实现进行替换。
可以测试松耦合,只需给 BraveKnight Mock一个具体Quest实现即可。
例如:注入一个Mock Quest来测试BraveKnight类

这里用Mock框架Mockito创建了一个Quest接口的Mock实现,通过Mock对象可以创建BraveKnight实例,并通过构造器注入这个Mock Quest。调用embarkOnQuest ()后,你可以用Mockito框架验证Mock的Quest的embark ()方法恰好被调用了一次。
verify检验mockQuest的embark方法是否调用了一次,如果是

测试通过。如果验证是否调用了两次,times(2),则

将Quest注入到Knight中
现在可以给BraveKight类传递任何Quest,如何传递指定的Quest?如果他的探险任务是杀恶龙,注入SlayDragonQuest

SlayDragonQuest类实现了Quest接口,适用BraveKnight。这里没有使用sout,而是通过构造器请求一个更通用的PrintStream。问题在于怎么把SlayDragonQuest给BraveKnight,怎么把PrintStream给SlayDragonQuest。
装配:创建应用组件之间协作的行为。Spring有很多装配Bean的方式,xml是常见的方式。下面展现Spring的配置,knights.xml,将这三个类装配到一起。
用spring配置文件把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" /> </bean> <bean id="quest" class="com.springinaction.knights.SlayDragonQuest">
<constructor-arg value="#{T(System).out}" /> </bean> </beans>
SlayDragonQuest与BraveKnight声明为Spring中的Bean:BraveKnight bean在构造时传入SlayDragonQuest bean的引用作为构造器的参数;SlayDragonQuest bean用了Spring表达式语言,将System.out(是一个PrintStream)传入SlayDragonQuest的构造器。
不喜欢xml的话,Spring提供了功能相同的基于java的配置:
package com.springinaction.knights.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.springinaction.knights.BraveKnight; import com.springinaction.knights.Knight; import com.springinaction.knights.Quest; import com.springinaction.knights.SlayDragonQuest; @Configuration public class KnightConfig { @Bean public Knight knight() { return new BraveKnight(quest()); } @Bean public Quest quest() { return new SlayDragonQuest(System.out); } }
无论哪种配置,获得的DI收益一样。尽管BraveKnight依赖Quest,但不知道所传递的Quest的类型,不知道这个Quest来自哪;SlayDragonQuest依赖PrintSream同理。
只有Spring通过它的配置才能知道各部分是怎么装配的。---------- >>>这样,可以不依赖类,就能修改依赖关系。
声明依赖关系后,只需装在XML配置文件,并启动应用。
观察它如何工作:
Spring通过应用上下文(Application Context)装在bean的定义,并把它们组装起来。Spring Application Context全权负责对象的创建和组装。Spring自带很多应用上下文的实现,区别仅仅是如何加载配置。
因为knights.xml的bean用xml配置,所以用ClassPathXmlApplicationContext类,作为应用上下文,用来加载路径下的一或多个xml配置文件。
下面,用ClassPathXmlApplicationContext加载knights.xml,并获取Knight对象的引用。

main基于knights.xml创建了Spring应用上下文;调用应用上下文获得id为knight的bean;得到Knight对象的引用,调用方法执行任务。
注:这个类完全不知接受什么任务,也不知道是BraveKnight来执行的。----->>>只有knights.xml 知道哪个骑士执行哪种任务。
1.1.3应用切面
左边业务与系统服务耦合过紧。关注点散到各个模块,而它们又不是各模块的核心业务。
AOP可使这些服务模块化,以声明的方式将他们应用到需要它们的组件中。------->>结果:高内聚,更加关注自身业务,不用了解系统服务的复杂性。
总之,AOP确保POJO的简单性。
切面是覆盖在多个组件上的一个外壳,由各业务模块组成。AOP用各功能层包裹各业务层组件(叠加),这些层以声明的方式灵活用到系统中,核心应用根本不知道他的存在。
强大的理念:安全,事务,日志关注点与核心业务逻辑分离。

利用AOP,将系统关注点覆盖在它们所影响的组件之上。
为骑士的例子加一个切面。使用吟游诗人服务类记载骑士的所与事迹,Minstrel类。探险前调用,探险后调用。
package com.springinaction.knights; import java.io.PrintStream; 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只有两个方法的简单类,骑士执行每个探险任务前调用singBeforeQuest(),之后调singAfterQuest()。Minstrel通过PrintStream类歌颂事迹,PrintStream通过构造器注入进来。
让BraveKnight使用Minstrel,
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(); minstrel.singAfterQuest(); } }
只需将Minisrel bean在Spring配置中注入到BraveKnight的构造器中。
But!
管理吟游诗人不是骑士的本职工作,吟游诗人应作他份内的事,不用骑士命令他。切注入吟游诗人让骑士代码复杂,而且是不是还需要一个不用吟游诗人的骑士呢?如果Minisrel为null是否要增加场景校验?
------->>复杂!AOP声明吟游诗人必须歌颂骑士的探险事迹,骑士不必直接访问Minisrel的方法。将Minisrel抽象为切面
-------->>要做的:在Spring配置文件中声明它,更新knights.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="knight" class="com.springinaction.knights.BraveKnight"> <constructor-arg ref="quest" /> </bean> <bean id="quest" class="com.springinaction.knights.SlayDragonQuest"> <constructor-arg value="#{T(System).out}" /> </bean> <bean id="minstrel" class="com.springinaction.knights.Minstrel"> //声明Minstrel bean <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> </beans>
使用Spring的aop配置命名空间把Minstrel bean声明为一个切面:
(1)把Minstrel声明成一个bean
(2)<aop:aspect>元素中引用该bean
(3)使用<aop:before>在embarkOnQuest()方法执行前调用Minstrel的singBeforeQuest()方法----------->>这种方式:前置通知
(4)相同方式:后置通知
两种方式,pointCut-ref都引用id为embark的切入点;切点在<pointCut>元素中定义,并配置expression属性选择应用的位置。表达式语法采用AspectJ的切点语法。
------->>结果:通过少量xml,把Minstrel声明为一个Spring切面
效果:
(1)Minstrel仍是一个POJO,没有代码表明它作切面使用。当经上述配置,在Spring上下文中,它已经是一个切面了。
(2)Minstrel被用到BraveKnight中,而BraveKnight不需显示调用它;BraveKnight根本不知道Minstrel的存在。
指出:Spring把Minstrel变为切面,首先把它声明为一个bean,所以Spring bean能做的事Spring切面也可以作,比如DI。
1.1.4使用模版消除样板式代码
为了实现简单通用的功能不得不重复写相似的代码------>>因为使用Java api导致的样板式代码,如JDBC访问DB查数据,员工
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 { 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查员工姓名与薪水:建DB链接,建语句对象,查询;捕捉JDBC异常;关闭连接,语句,结果集;捕捉JDBC异常。
与其他JDBC基本相同,除了查询员工的逻辑,其他代码都是JDBC的样板代码。
JDBC不是产生样板代码的唯一场景,REST服务等也会涉及。
----->>Spring通过模版封装消除样板代码,Spring的JDBC Template是执行数据库操作时避免样板代码。
如下,仅仅关注与员工相关的逻辑,不用迎合JDBC api的需求。
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); }
模版让你的代码更专注自身业务,仅关注查询员工。
使用模版需要:(1)sql查询语句(2)RowMapper对象把数据映射为域对象(3)0或多个查询参数
样板代码被封装到模版中。
1.2容纳你的Bean
基于Spring的应用,应用对象都生存与Spring容器(container)中。

Spring容器负责创建对象,装配它们,配置它们,并管理它们整个生命周期。
容器是Spring框架的核心,容器使用DI管理构成应用的组件,创建相互协作的组件之间的联系。
Spring容器不止一个,Spring自带了很多容器实现,两个类型:
(1)bean工厂(由org.springframework.beans.factory.BeanFactory 接口定义),最简单的容器,提供基本的DI支持;
(2)应用上下文(org.springframework.context.ApplicationContext 接口定义),基于Bean工厂构建,提供应用框架级别的服务,如通过属性文件解析文本信息,如发布应用事件给感兴趣的监听者。
Bean工厂太低级,应用上下文更好。
1.2.1使用应用上下文
Spring自带多种类型上下文,常见的:
(1)AnnotationConfigApplicationContext:从一个或多个基于Java的配置类中加载Spring应用上下文;
(2)AnnotationConfigWebApplicationContext:从一个或多个基于Java的配置类中加载Spring web应用上下文;
(3)ClassPathXmlApplicationContext:从类路径下的一或多个xml配置文件中加载上下文定义,把应用上下文的定义文件作为资源类;
(4)FileSystemXmlApplicationContext:从文件系统下的一或多个xml配置文件中加载上下文定义;
(5)XmlWebApplicationContext:从web应用下的一或多个xml配置文件中加载上下文定义。
第8章基于web的Spring应用详细讨论(2)与(5),现在简单使用(3)(4)加载应用上下文。
从文件系统装载上下文与类路径装在上下文,将bean加载到bean工厂的过程类似。
如加载FileSystemXmlApplicationContext,
ApplicationContext context = new FileSystemXmlApplicationContext("c:/knight.xml");
如加载ClassPathXmlApplicationContext,
ApplicationContext context = new ClassPathXmlApplicationContext("knight.xml");
区别:FileSystemXmlApplicationContext在指定文件系统路径下查找knight.xml,ClassPathXmlApplicationContext在所有类路径下(包括JAR文件)查找knight.xml。
如果想从java配置中加载应用上下文,用AnnotationConfigApplicationContext:
ApplicationContext context = new AnnotationConfigApplicationContext(com.springinaction.knights.config.KnightConfig.class);
这里没指定加载Spring应用上下文所需的xml文件,AnnotationConfigApplicationContext通过一个配置类加载bean。
上下文就绪,可调用上下文的getBean()方法从Spring容器中获取bean。
已讲述如何创建Spring容器,探讨lifetime。
1.2.2bean的生命周期
传统Java应用,bean周期简单:new进行bean的实例化--->使用bean--->bean不再使用,java自动垃圾回收。
Spring容器中的bean复杂,正确理解Spring bean的生命周期很重要。因为需要利用Spring的扩展点自定义bean的创建过程;bean在Spring容器从创建到销毁经历若干阶段,每个阶段都可个性化定制Spring如何管理bean。

bena准备就绪前,bean工厂执行了若干启动步骤:
(1)Spring对bean进行实例化
(2)Spring将值与bean的引用注入到bean对应的属性
(3)如果bean实现了BeanNameAware 接口,Spring传递bean的id给setBeanName()方法
(4)如果bean实现了BeanFactoryAware 接口,Spring调用setBeanFactory ()方法,将bean factory容器实例传入
(5)如果bean实现了ApplicationContextAware 接口,Spring调用setApplicationContext ()方法,将bean 所在的应用上下文的引用传入
(6)如果bean实现了BeanPostProcessor 接口,Spring调用postProcessBeforeInitialization ()方法
(7)如果bean实现了InitializingBean 接口,Spring调用afterPropertiesSet ()方法。类似,如果bean被init-method 初始化方法声明,该方法也会被调用
(8)如果bean实现了BeanPostProcessor 接口,Spring调用postProcessAfterInitialization ()方法
(9)此时bean已准备就绪,可被程序使用,它们将一直在应用上下文中,知道应用上下文被销毁
(10)如果bean实现了DisposableBean 接口,Spring调到destroy ()方法;同样,若bean使用destroy-method声明了销毁方法,该方法也会被调用
到此,学了如何创建和加载Spring容器,但空容器无意义,需要将应用对象装配进Spring容器,第二章探讨。
1.3.1Spring模块
上述模块按功能分为6块,不必将你的应用建在整个Spring框架上,选择自己所用的模块。Spring提供了与第三方框架和类库的集成点,不用自己写。

(1)Spring核心容器
容器是Spring框架的核心,

浙公网安备 33010602011771号