《Spring实战》学习笔记-1

111 阅读15分钟

Spring是为解决企业级开发的复杂性而创建的,是为了简化Java开发,使用Spring可以让简单的JavaBean实现很多之前只有EJB才能实现的事情。一个Spring组件可以是任何形式的POJO,并不一定要遵循JavaBean规范。

引言

JavaBean

定义

JavaBean是为了能够重复使用而设计的程序段落,而且这些段落并不只服务于某一个程序,当某个程序需要某个功能的时候就可以调用它,JavaBean方便了重复使用一些程序,因此JavaBean是一种符合一定规范的Java类。

分类

JavaBean可以分成业务Bean和实体Bean。 实体Bean主要是对系统中使用到的数据进行封装,包括前端输入的数据和数据库中读取到的数据。

JavaBean规范

  1. JavaBean必须是一个公共类,并且访问属性应该是public。
  2. JavaBean必须要有一个空的构造方法。
  3. JavaBean的所有成员变量要用private修饰,并且要为每个成员变量提供访问属性为public的get和set方法。
  4. 成员变量应该通过一组存取方法来访问,这里如果成员变量是布尔类型,可以使用isXXX代替get方法。

面向对象的封装性

JavaBean规范体现了面向对象的封装性,面向对象的封装性是指利用抽象数据类型,将数据和基于数据的操作封装在一起,使其形成一个不可分割的独立实体,这样数据就可以被保护在类的内部以尽可能地隐藏相关细节,只保留一些对外接口与外部发生联系。

Spring关键策略

为了降低Java开发的复杂度,Spring有以下4种关键策略

  • 基于POJO的轻量级和最小侵入式编程
  • 通过依赖注入和面向接口实现松耦合
  • 基于切面和惯例进行声明式编程
  • 通过切面和模板减少样板式代码

基于POJO的轻量级和最小侵入式编程

侵入式编程是指框架强迫应用继承它们的类或者实现它们的接口导致应用与框架绑定,而Spring则不会强迫用户实现Spring规范的接口或者继承Spring规范的类,在基于Spring构建的应用中,一个类没有痕迹表明你使用了Spring,即使一个类使用了Spring注解,它依旧是一个POJO。

通过依赖注入和面向接口实现松耦合

现在的应用都是由很多类组成的,往往这些类需要进行协作来完成一些特定的业务逻辑,如果每个对象自行管理与自己有协作的对象的引用,会导致代码高度耦合。以书中的代码为例:

package com.springinaction.knights;

public class DamselRescuingKnight implements Knight {
    private RescueDamselQuest quest;
    
    public DamselRescuingKnight() {
        this.quest = new RescueDamselQuest();
    }
    
    public void embarkOnQuest() {
        quest.embark();
    }
}

在DamselRescuingKnight类中自行创建了一个RescueDamselQuest的对象quest,这样DamselRescuingKnight类就和RescueDamselQuest类紧密地耦合在一起,DamselRescuingKnight就只能与RescueDamselQuest进行协作,如果要再与其它类进行协作,就需要在DamselRescuingKnight类中再创建一个新的对象。并且DamselRescuingKnight很难进行测试,在调用embarkOnQuest方法的时候,你必须确保embark方法能被调用。 当然,一定的耦合是必要的,如果没有耦合的代码,是没有意义的,为了完成有实际意义的代码,不同的类要有一定的交互,但是必须要小心谨慎地管理耦合。

通过依赖注入(DI),可以将所依赖的关系自动交给目标对象,而不是让对象自己去获取依赖,即对象的依赖关系会在创建对象时进行设定,对象自己不需要自行创建或者管理它的依赖。以书中的代码为例:

package com.springinaction.knights;

public BraveKnight implements Knight {
    private Quest quest;
    
    public BraveKnight(Quest quest) {
        this.quest = quest;
    }
    
    public void embarkOnQuest() {
        quest.embark();
    }
}

上面的代码是依赖注入的一种方式之一:构造器注入。这段代码在创建对象的时候将quest作为构造器的参数传入,而不是由对象自行创建具体的quest。这里传入的探险类型是Quest,它是所有探险任务必须实现的一个接口。这样对象就不用创建具体的探险类型,就实现了松耦合。

现在BraveKnight类可以接收任何一种Quest的实现,但是我们如何把特定的Query传递给它?比如下面的例子:

package com.springinaction.knights;

import java.io.PrintStream;

public class SlayDragonQuest implements Quest {
  private PrintStream stream;

  public SlayDragonQuest(PrintStream stream) {
    this.stream = stream;
  }

  public void embark() {
    stream.println("Embarking on quest to slay the dragon!");
  }
}

SlayDragonQuest实现了Quest接口,那么它就可以注入到BraveKnight中去。那么如何将SlayDragonQuest交给BraveKnight呢?又如何把PrintStream交给SlayDragonQuest呢?

创建应用程序之间协作的行为称为装配,Spring有多种装配方式,采用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"
  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>

这里BraveKnight和SlayDragonQuest被声明为Spring的bean,在声明BraveKnight的时候,传入了构造器参数quest的引用,而SlayDragonQuest在声明时使用了Spring表达式语言,将System.out作为构造器参数传入。

装配的另外一种方式是基于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);
  }
}

不管是哪种方式,依赖注入的收益都是一样的。尽管BraveKnight依赖于Quest,但是并不需要知道传递给它的是哪种Quest,在编码的时候就可以不修改所依赖的类,修改依赖关系。接下来以XML方式为例,进行装载XML配置文件,把应用启动起来。

Spring通过应用上下文装载Bean的定义并把它们组装起来,Spring应用上下文全权负责对象的创建和组装,Spring自带了多种应用上下文的实现,它们之间的区别仅仅在于如何加载配置。因为knights.xml中的bean使用xml方式进行配置,使用ClassPathXmlApplicationContext作为应用上下文相对比较合适,该类加载位于应用程序路径下的一个或多个xml配置文件;对于基于Java的配置,可以使用AnnotationConfigApplicationContext。

package com.springinaction.knights;

import org.springframework.context.support.
                   ClassPathXmlApplicationContext;

public class KnightMain {

  public static void main(String[] args) throws Exception {
    ClassPathXmlApplicationContext context = 
        new ClassPathXmlApplicationContext(
            "META-INF/spring/knight.xml"); // 加载Spring上下文
    Knight knight = context.getBean(Knight.class); // 获取knight bean
    knight.embarkOnQuest(); // 使用knight
    context.close();
  }
}

这里的main方法基于knights.xml文件创建了Spring应用上下文,并通过上下文获取到了Knight对象的引用,然后进行embarkOnQuest方法的调用就执行了所赋予的探险任务,这里knight对象并不知道接受的是哪种探险任务,也并不知道是由BraveKnight来执行的,只有knights.xml知道具体的细节。

基于切面和惯例进行声明式编程

依赖注入可以让相互协作的组件保持松耦合,面向切面编程可以把遍布应用各处的功能分离出来形成可以重用的组件。AOP可以将各个服务模块化,并且以声明的方式将它们应用到需要影响的组件中去,所造成的结果就是这些组件会具有更高的内聚性并且会更加关注自己的业务,完全不需要了解涉及系统服务所带来的复杂性,即AOP可以保证POJO的简单性。 以书中的例子为例:

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!");
  }
}

在BraveKnight执行embark之前,singBeforeQuest需要被调用,执行之后singAfterQuest需要被调用,这两种情况下,Minstrel都会通过一个PrintStream类来歌颂骑士的事迹,这个类是通过构造器方法来实现的。按照之前依赖注入的写法,我们可以这么写:

package com.springinaction.knights;
  
public class BraveKnight implements Knight {

  private Quest quest;
  private Minstrel minstrel;

  public BraveKnight(Quest quest) {
    this.quest = quest;
  }

  public void embarkOnQuest() {
    minestrel.singBeforeQuest();
    quest.embark();
    minestrel.singAfterQuest();
  }
}

接下来就是要在xml中声明并注入bean,似乎可以实现功能,但是我们会有下面的问题:Minstrel类真的应该由BraveKnight管理吗?这样原本简单的BraveKnight就变得复杂起来。但是如果利用AOP,我们可以声明Minstrel类,且BraveKnight中并不需要直接执行Minstrel的方法,更新后的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.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 ref="fakePrintStream" />
  </bean>

  // 声明minstrel的bean
  <bean id="minstrel" class="com.springinaction.knights.Minstrel">
    <constructor-arg ref="fakePrintStream" />
  </bean>

  <bean id="fakePrintStream" class="com.springinaction.knights.FakePrintStream" />

  <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声明为一个切面,声明为一个切面首先要把它定义为一个bean,并且要在aspect中引用它,为了进一步定义切面,可以声明aop:before等方法。 这样的做法使用少量xml配置就可以把Minstrel声明为一个切面,但是Minstrel本身还是一个POJO,没有代码表明它是一个切面,但是当我们配置完成之后,Minstrel实际上已经变成一个切面了,并且Minstrel可以被应用到BraveKnight中去,且BravelKnight不需要显式地调用它。

通过切面和模板减少样板式代码

我们可能经常会觉得,写过的代码之前写过,这是因为我们为了实现通用简单的功能,不得不一遍遍地重复编写这样的代码。它们中的很多是因为使用了Java API而导致的样板式代码,Spring则希望通过模板封装来消除这些样板式代码。

Spring容器

在基于Spring的应用中,应用对象生存于Spring容器中,Spring容器负责创建、装配、配置对象并管理对象的生命周期。容器是Spring框架的核心,Spring容器使用DI管理构成应用的组件,它会创建相互协作的组件之间的关联,这样的对象更加方便管理。 Spring的容器并不是只有一个,Spring自带多个容器的实现,可以分成两种类型。bean工厂是最简单的容器,由org.springframework.beans.factory.eanFactory接口定义,提供基本的依赖注入;应用上下文是另外一种,由org.springframework.context.ApplicationContext接口定义,基于BeanFactory构建,并提供框架级别的服务,比如从属性文件中解析文本信息以及发布应用事件给感兴趣的事件监听者。通常情况下,由于bean工厂往往对大多数应用来说太低级了,我们主要使用应用上下文。

使用应用上下文

Spring自带了多种类型的应用上下文,如:

  • AnnotationConfigApplicationContext:从一个或多个基于Java的配置类中加载Spring应用上下文。
  • AnnotationConfigWebApplicationContext:从一个或多个基于Java的配置类中加载Spring Web应用上下文。
  • ClassPathXmlApplicationContext:从类路径下的 一个或多个XML配置文件中加载上下文定义,把应用上下文的定义文件作为类资源。
  • FileSystemXmlApplicationContext:从文件系统下的一个或多个XML配置文件中加载上下文定义。
  • XmlWebApplicationContext:从Web应用下的一个或多个XML配置文件中加载上下文定义。

不管用哪种方法装载应用上下文,将bean加载到bean工厂的过程都是类似的。

ApplicationContext context = new
    FileSystemXmlApplicationContext("c:/knight.xml"); // 从文件系统加载上下文
    
ApplicationContext context = new
    ClassPathXmlApplicationContext("knight.xml"); // 从类路径下加载上下文

ApplicationContext context = new AnnotationConfigApplicationContext(com.springinaciton.knights.config.KnightConfig.class); // 从Java配置中加载应用上下文

bean的生命周期

在传统的Java应用中,bean的生命周期很简单,使用Java的关键字new进行实例化就可以使用bean了,一旦它不再使用,Java会自动进行垃圾回收。 而Spring容器的bean的生命周期则要复杂很多。以书中的图为例: image.png 我们可以以react的生命周期类比,Spring的bean从创建到销毁经历了很多阶段,每个阶段都可以进行个性化定制。具体步骤如下:

  1. Spring对bean进行实例化
  2. Spring将值和bean的引用注入到bean对应的属性中
  3. 如果bean实现了BeanNameAware接口,Spring将bean的ID传递给setBeanName()方法
  4. 如果bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入
  5. 如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext()方法,将bean所在的应用上下文引用传入进来
  6. 如果bean实现了BeanPostProcessor接口,Spring将调用它们的postProcessAfterInitialization()方法
  7. 如果bean实现了InitializingBean接口,Spring将调用它们的afterPropertiesSet()方法。类似地,如果bean使用initmethod声明了初始化方法,该方法也会被调用
  8. 如果bean实现了BeanPostProcessor接口,Spring将调用它们的postProcessAfterInitialization()方法
  9. 此时,bean已经准备就绪,可以被应用程序使用,它们将一直驻留在应用上下文中,知道该应用上下文被销毁
  10. 如果bean实现了DisposableBean接口,Spring将调用它的destroy()接口方法。同样,如果bean使用destroymethod声明了销毁方法,该方法也会被调用

在容器创建和加载之后,我们就需要将对象装配到容器中。

Spring框架

Spring框架通过DI、AOP等简化了企业级开发, 但它不止这些,那么Spring框架的核心是什么呢?

Spring模块

当下载下Spring的release版本时,我们可以看到lib目录下有多个jar文件,这些就是Spring的不同模块,按照功能可以划分为六类:

  • 数据访问与继承:JDBC、Transaction、ORM、OXM、Message、JMS
  • Web和远程调用:Web、Web servlet、Web portlet、WebSocket
  • 面向切面编程:AOP、Aspects
  • Instrumentation:Instrument、Instrument Tomcat
  • Spring核心容器:Beans、Core、Context、Expression、Context support
  • 测试:Test

Spring核心容器

Spring框架最核心的部分,它管理Spring应用中bean的创建、应用和管理。该模块中包括了Spring bean工厂,它为Spring提供了DI的功能。还有多种基于bean工厂的Spring应用上下文的实现,每一种提供了配置Spring的不同方式。Spring核心容器除了bean工厂和应用上下文,也提供了许多企业服务,例如Email、JNDI访问、EJB集成和调度。所有的Spring模块都构建于核心容器之上。

Spring的AOP模块

在AOP模块中,Spring对面向切面编程提供了丰富的支持。这个模块是Spring应用系统中开发切面的基础。与DI一样,AOP可以帮助应用对象解耦。将遍布系统的关注点(例如事务和安全)从它们所应用的对象中解耦出来。

数据访问和集成

使用JDBC编写代码通常会导致大量的样板式代码,Spring的JDBC和DAO模块抽象了这些样板式代码,使数据库代码变得简单明了,还可以避免因为关闭数据库资源失败而引发的问题。该模块在多种数据库服务的错误信息之上构建了一个语义丰富的异常层。对于更喜欢ORM工具而不愿意直接使用JDBC的开发者,Spring提供了ORM模块。Spring的ORM模块建立在对DAO的支持之上,并为多个ORM框架提供了一种构建DAO的简便方式。Spring对许多流行的ORM框架进行了继承。。Spring的事务管理支持所有的ORM框架以及JDBC。本模块同样包含了在JMS之上构建的Spring抽象层,使消息以异步的方式与其他应用集成。除此之外,本模块会使用Spring AOP模块为Spring应用中的对象提供事务管理服务。

Web与远程调用

Spring虽然能够与多种流行的MVC框架集成,但它的Web和远程调用模块自带了一个强大的MVC框架,有助于在Web层提升应用的松耦合水平。除了面向用户的Web应用,该模块还提供了多种构建与其他应用交互的远程调用方案。

Instrumentation

Spring的Instrumentation模块提供了为JVM添加代理的功能。为Tomcat提供了一个织入代理,能够为Tomcat传递类文件,就像是这些文件被类加载器加载一样。Instrumentation的使用场景非常有限,不会具体介绍。

测试

Spring提供了测试模块以致力于Spring应用的测试。Spring为使用JNDI、Sevlet和Portlet编写单元测试提供了一系列的mock对象实现。对于集成测试,测试模块为加载Spring应用上下文的bean集合以及与Spring上下文中的bean进行交互提供了支持。

Spring Portfolio

Spring远远不是Spring框架下载的那些,整个Spring Portfolio包括多个构建在Spring框架上的框架和类库,它为每个领域的Java开发都提供了Spring编程模型,如

  • Spring Web flow:建立在Spring MVC之上,为基于流程的会话式Web应用提供了支持。
  • Spring Web Service
  • Spring Security:利用Spring AOP,Spring Security为Spring应用提供了声明式的安全机制
  • Spring Integration
  • Spring Batch
  • Spring Data
  • Spring Social
  • Spring Mobile
  • Spring for Android
  • Spring Boot