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应用切面

 

DI让互相协作的组件松耦合,而面向切面编程(AOP)允许你把遍布应用各处的功能分离出来形成可重用的组件。
AOP是使系统实现关注点分离的技术。系统由多组件组成,每个负责一块特定功能;除自身核心功能,每个组件还有额外职责:日志,事务管理,安全等系统服务,经常融入有自身核心业务逻辑的组件中,这些系统服务成为横切关注点,因为它们会跨越系统的多个组件。
如果关注点分散在多个组件,代码会双重复杂性:
(1)关注点代码重复出现在多个组件。----->>改变关注点逻辑,必须修改每个相关模块的实现;即使关注点抽象为一个模块,其他模块只是调用它,方法的调用还是会重复出现在各个组件。
(2)各组件因引入与自身核心代码无关的代码变得混乱。业务只需关心业务,不许关心安全与是否支持事务。
 

 左边业务与系统服务耦合过紧。关注点散到各个模块,而它们又不是各模块的核心业务。

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俯瞰Spring风景线
Spring:DI,AOP,消除样板代码,简化企业java开发。
1.3.1Spring模块
Spring4.0的lib下,20个模块,每个模块3个JAR文件(二进制类库,源码JAR文件,JavaDoc JAR文件)。

上述模块按功能分为6块,不必将你的应用建在整个Spring框架上,选择自己所用的模块。Spring提供了与第三方框架和类库的集成点,不用自己写。

(1)Spring核心容器

容器是Spring框架的核心,

 

 
 
posted @ 2018-12-25 15:22  rOtk1992  阅读(138)  评论(0)    收藏  举报