Skip to content

2. 第 1 章 - Spring IoC

本文目标

  • 探索 Spring 框架(http://www.springframework.org)的配置与集成可能性
  • 定义并应用 IoC(控制反转,Inversion of Control)的概念,也称为依赖注入(Dependency Injection)

2.1. 使用 Spring 配置三层应用

考虑一个典型的三层应用程序:

假设对业务层和DAO的访问由Java接口控制:

  1. 用于数据访问层的接口 [IArticlesDao]
  2. 业务层接口 [IArticlesManager]

在数据访问层(即 DAO 层,即数据访问对象)中,通常会使用 SGBD,因此也会使用 JDBC 驱动程序。 以下是一个在 SGBD 中访问物料表的类框架:

public class ArticlesDaoPlainJdbc implements IArticlesDao {

     // 连接到数据源
    private String driverClassName=null;
    private Connection connexion=null;
    private String url = null;
    private String user = null;
    private String pwd = null;
 ....

    public List getAllArticles() {
        // 查询商品列表
        try {
             // 加载驱动程序 JDBC
            Class.forName(driverClassName);
            // 建立连接至 BD
            connexion = DriverManager.getConnection(url, user, pwd);
            ...
        } catch (SQLException ex) {
            ...
        } finally {
            ...
        }
    }

要在 SGBD 上执行操作,任何方法都需要一个 [Connection] 对象,该对象代表与数据库的连接,数据库与 Java 代码之间的数据交换将通过此连接进行。要构建该对象,需要以下四项信息:

String driverClassName
JDBC驱动程序的类名SGBD
String url
要使用的数据库的 URL
String user
用于建立连接的用户名
String pwd
该身份的密码

我们之前的 [ArticlesDaoPlainJdbc] 类如何获取这些信息?有以下几种可能:

方案 1 - 信息硬编码在类中:

1
2
3
4
5
6
7
8
public class ArticlesDaoPlainJdbc implements IArticlesDao {

     // 连接到数据源
    private final String driverClassName = "org.firebirdsql.jdbc.FBDriver";
    private String url = "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb";
    private String user = "someone";
    private String pwd = "somepassword";
 ....

此方案的缺点在于,一旦信息发生变更(例如密码更改),就必须修改Java代码。

方案 2 - 信息在对象构造时传递给该对象:

public class ArticlesDaoPlainJdbc implements IArticlesDao {

     // 连接到数据源
    private final String driverClassName;
    private String url;
    private String user;
    private String pwd;
 ....
    public ArticlesDaoPlainJdbc(String driverClassName,String url,String user,String pwd) {
      this.driverClassName=driverClassName;
    this.url=url;
    this.user=user;
    this.pwd=pwd;
    ...
    }

在此,对象在构造时会接收其运行所需的信息。问题便转移到了向其传递这四项信息的代码上。它是如何获得这些信息的?业务层的下一个类 [ArticlesManagerWithDataBase] 可能会构造数据访问层的对象 [ArticlesDaoPlainJdbc]:

public class ArticlesManagerWithDataBase implements IArticlesManager {

    // 一个数据访问实例
    private IArticlesDao articlesDao;
 ....
    public ArticlesManagerWithDataBase (String driverClassName, String url, String user, String pwd, ...) {
        ... 
         // 创建数据访问服务
        articlesDao =(IArticlesDao)new ArticlesDaoPlainJdbc(driverClassName,url,user,pwd);
    ...
    }

    public ... doSomething(...){
        ...
    }
}

可见,在此处,构建对象 [ArticlesDaoPlainJdbc] 所需的信息同样被提供给了对象 [ArticlesManagerWithDataBase] 的构造函数。可以设想,这些信息是由上一层(例如用户界面层)传递给它的。 就这样,我们一步步追溯到了应用程序的最高层。由于其位置,该层不会被能够向其传递所需配置信息的层所调用。 因此必须寻找除构造函数配置之外的替代方案。在应用程序最高层进行配置的常规做法是使用配置文件,其中包含所有可能随时间变化的信息。此类文件可能有多个。应用程序启动时,初始化层将创建应用程序各层所需的所有或部分对象。

配置文件种类繁多。当前的趋势是使用 XML 文件。这是 Spring 采用的方案。配置 [ArticlesDaoPlainJdbc] 对象的文件可能如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- 数据访问类 -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
        <constructor-arg index="0">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>someone</value>
        </constructor-arg>
        <constructor-arg index="3">
            <value>somepassword</value>
        </constructor-arg>
    </bean>
</beans>

应用程序是一组对象,Spring 将其称为 Bean,因为它们遵循 JavaBean 标准,该标准规定了对象私有字段的访问器(getter/setter)命名规则。在应用程序中,那些负责提供服务的对象通常只创建一个实例。 这类对象被称为单例。因此,在我们此处探讨的多层应用程序示例中,对商品数据库的访问将由 [ArticlesDaoPlainJdbc] 类的唯一实例来处理。对于 Web 应用程序,这些服务对象会同时为多个客户端提供服务。我们不会为每个客户端创建一个服务对象。

上面的 Spring 配置文件允许在名为 [istia.st.articles.dao] 的包中创建一个 [ArticlesDaoPlainJdbc] 类型的唯一服务对象。 该对象构造器所需的四项信息定义在 <bean>...</bean> 标签内。需要构建的单例有多少,就应有相应数量的 <bean> 标签。

Spring配置文件中定义的对象何时进行构建?应用程序的初始化工作通常由该应用程序的main方法负责(如果该应用程序包含main方法)。对于Web应用程序,则可能由主Servlet的[init]方法负责。 在任何应用程序中,都存在一个必定最先执行的方法。通常,单例的构建就在该方法中进行。

举个例子。假设我们要使用测试方法 JUnit 来测试前面的类 [ArticlesDaoPlainJdbc]。 测试类 JUnit 包含一个名为 [setUp] 的方法,该方法会在其他所有方法之前执行。我们将在该方法中创建单例 [ArticlesDaoPlainJdbc]。

如果采用通过构造函数传递配置信息的方案,则测试类如下所示:

public class TestArticlesPlainJdbc extends TestCase {
    // 测试物料访问类ArticlesDaoPlainJdbc
     // 数据源在sprintest中定义

     // 被测试类的实例
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception{
        // 获取数据访问实例
        articlesDao =
            (IArticlesDao) new ArticlesDaoPlainJdbc("org.firebirdsql.jdbc.FBDriver",
                "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb","someone","somepassword");
    }

调用类 [TestArticlesPlainJdbc] 必须知道初始化待构建的单例 [ArticlesDaoPlainJdbc] 所需的四项信息。

如果采用通过配置文件传递配置信息的方案,使用上述 Spring 配置文件,可能会得到如下测试类:

public class TestSpringArticlesPlainJdbc extends TestCase {
    // 测试项目访问类ArticlesDaoJdbc
     // 数据源在Sprintest中定义

     // 被测试类的实例
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
      // 获取数据访问实例
      articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
          "springArticlesPlainJdbc.xml"))).getBean("articlesDao");
    }

在此情况下,调用类 [TestSpringArticlesPlainJdbc] 无需了解构建该单例所需的初始化信息。它只需知道:

  1. [springArticlesPlainJdbc.xml]:上文所述的 Spring 配置文件名称
  2. [articlesDao]:待创建单例的名称

除了上述这两个实体之外,对配置文件的任何修改都不会对 Java 代码产生影响。这种应用程序对象的配置方法非常灵活。为了进行配置,它只需要知道两件事:

  • 包含待构建单例定义的 Spring 文件名
  • 这些单例的名称,Java 代码将通过配置文件利用这些名称获取与其关联的对象引用

2.2. 依赖注入与控制反转

现在我们来介绍 Spring 用于配置应用程序的依赖注入(Dependency Injection)概念。该概念也常被称为控制反转(Inversion of Control)。让我们以构建应用程序业务层中的单例 [ArticlesManagerWithDataBase] 为例:

为了访问 SGBD 中的数据,业务层必须使用实现 [IArticlesDao] 接口的对象提供的服务,例如 [ArticlesDaoPlainJdbc] 类型的对象。 [ArticlesManagerWithDataBase]类的代码可能如下所示:

public class ArticlesManagerWithDataBase implements IArticlesManager {

     // 数据访问实例
    private IArticlesDao articlesDao;
 ....
    public ArticlesManagerWithDataBase (String driverClassName, String url, String user, String pwd, ...) {
        ... 
         // 创建数据访问服务
        articlesDao =(IArticlesDao)new ArticlesDaoPlainJdbc(driverClassName,url,user,pwd);
    ...
    }

    public ... doSomething(...){
        ...
    }
}

此处假设类 [ArticlesDaoPlainJdbc] 实现了接口 [IArticlesDao]:

public class ArticlesDaoPlainJdbc implements IArticlesDao {...}

为了创建类运行所需的 [IArticlesDao] 类型的单例,该类的构造函数显式使用了接口 [IArticlesDao] 的实现类名称:

articlesDao =(IArticlesDao) new ArticlesDaoPlainJdbc(...);

因此,代码中对类名存在硬编码依赖。如果接口 [IArticlesDao] 的实现类发生变更,则上述构造函数的代码必须进行修改。对象之间存在以下关系:

类 [ArticlesManagerWithDataBase] 会主动创建其所需的对象 [ArticlesDaoPlainJdbc]。回到“控制反转”这一术语,可以说正是它掌握着创建所需对象的“控制权”。

如果我们要编写一个用于测试类 [ArticlesManagerWithDataBase] 的测试类 JUnit,代码可能如下所示:

public class TestArticlesManagerWithDataBase extends TestCase {
    // 被测业务类的实例
    private IArticlesManager articlesManager;

    protected void setUp() throws Exception {
        // 创建被测业务类的实例
        articlesManager =
            (IArticlesManager) new ArticlesManagerWithDataBase("org.firebirdsql.jdbc.FBDriver",
                "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb","someone","somepassword");
    }

测试类创建了业务类 [ArticlesManagerWithDataBase] 的实例,该业务类又在其构造函数中创建了数据访问类 [ArticlesDaoPlainJdbc] 的实例。

使用 Spring 的解决方案将消除业务类 [ArticlesManagerWithDataBase] 对所需数据访问类名称 [ArticlesDaoPlainJdbc] 的依赖。这使得在无需修改业务类 Java 代码的情况下即可更换该类。 Spring将同时创建数据访问层和业务层的两个单例。Spring配置文件将定义一个新的Bean:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- 数据访问类 -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
        <constructor-arg index="0">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>someone</value>
        </constructor-arg>
        <constructor-arg index="3">
            <value>somepassword</value>
        </constructor-arg>
    </bean>
    <bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
        <property name="articlesDao">
            <ref bean="articlesDao"/>
        </property>
    </bean>
</beans>

创新之处在于定义待创建业务类单例的 Bean:

    <bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
        <property name="articlesDao">
            <ref bean="articlesDao"/>
        </property>
    </bean>
  1. 实现 Bean [articlesManager] 的类已定义:[ArticlesManagerWithDataBase]
  2. Bean 的字段 [articlesDao] 通过标签 <property name="articlesDao"> 接收值。该字段在类 [ArticlesManagerWithDataBase] 中定义:
public class ArticlesManagerWithDataBase implements IArticlesManager {

  // 数据访问接口
  private IArticlesDao articlesDao;

  public IArticlesDao getArticlesDao() {
    return articlesDao;
  }

  public void setArticlesDao(IArticlesDao articlesDao) {
    this.articlesDao = articlesDao;
  }

为了使字段 [articlesDao] 能通过 Spring 及其 <property> 标签进行初始化, 该字段必须遵循 JavaBean 规范,并且必须存在一个 [setArticlesDao] 方法来初始化 [articlesDao] 字段。请注意该方法的名称,它是根据字段名称精确派生而来的。 与此相应,通常还存在一个 [get...] 方法用于获取该字段的值。在此示例中,该方法为 [getArticlesDao]。在此新版本中,[ArticlesManagerWithDataBase] 类不再拥有构造函数,因为它已不再需要。

  • Spring将赋予字段[articlesDao]的值,即其配置文件中定义的Bean [articlesDao]的值:
    <bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
        <property name="articlesDao">
            <ref bean="articlesDao"/>
        </property>
    </bean>
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
        <constructor-arg index="0">
    .............
    </bean>
  • 当 Spring 构建单例 [ArticlesManagerWithDataBase] 时,它也会创建单例 [ArticlesDaoPlainJdbc]:
    • Spring 将建立一个 Bean 依赖关系图,并发现 Bean [articlesManager] 依赖于 Bean [articlesDao]
    • 它将构建 Bean [articlesDao],即类型为 [ArticlesDaoPlainJdbc] 的对象
    • 随后,它将构建类型为 [ArticlesManagerWithDataBase] 的 Bean [articlesManager]

现在假设有一个针对类 [ArticlesManagerWithDataBase] 的测试 JUnit。它可能如下所示:

public class TestSpringArticlesManagerWithDataBase extends TestCase {
    // 测试业务类[ArticlesManagerWithDataBase]

    // 被测试业务类的实例
    private IArticlesManager articlesManager;

    protected void setUp() throws Exception {
      // 获取数据访问实例
      articlesManager = (IArticlesManager) (new XmlBeanFactory(new ClassPathResource(
          "springArticlesManagerWithDataBase.xml"))).getBean("articlesManager");
    }

让我们追踪一下在名为 [springArticlesManagerWithDataBase.xml] 的 Spring 文件中定义的两个单例的创建过程。

  • 上文中的 [setUp] 方法请求名为 [articlesManager] 的 Bean 的引用
  • Spring 查询其配置文件,找到 Bean [articlesManager]。如果该 Bean 已存在,则直接返回该对象(单例)的引用;否则,则创建该 Bean。
  • Spring 检测到 Bean [articlesManager] 对 Bean [articlesDao] 的依赖。 因此,如果 [articlesDao] 类型的单例尚未创建(单例),则会创建该单例。
  • 它创建了类型为 [ArticlesManagerWithDataBase] 的单例 [articlesManager]

该机制可概括如下:

回顾类 [ArticlesManagerWithDataBase] 的结构:

public class ArticlesManagerWithDataBase implements IArticlesManager {

  // 数据访问接口
  private IArticlesDao articlesDao;

  public IArticlesDao getArticlesDao() {
    return articlesDao;
  }

  public void setArticlesDao(IArticlesDao articlesDao) {
    this.articlesDao = articlesDao;
  }

在 Spring 完成单例对象的构建后,我们得到一个类型为 [ArticlesManagerWithDataBase] 的对象,其字段 [articlesDao] 已被初始化,而该对象自身并不知晓初始化过程。这被称为向 [ArticlesManagerWithDataBase] 对象注入了依赖。 这也被称为控制反转:不再是由对象 [ArticlesManagerWithDataBase] 主动创建其所需的、实现接口 [IArticlesDao] 的对象, 而是最高级别的应用程序(在初始化时)负责创建其所需的所有对象,并管理这些对象之间的依赖关系。

通过 Spring 文件配置单例 [ArticlesManagerWithDataBase] 的主要优势在于:现在我们可以更改类 [ArticlesManagerWithDataBase] 中字段 [articlesDao] 对应的实现类,而无需修改该类的代码。 只需在 Spring 文件中修改 [articlesDao] Bean 的定义中的类名即可:

    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
...
    </bean>

例如将变为:

    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoIbatisSqlMap">
...
    </bean>

[ArticlesManagerWithDataBase] 这个 Bean 将使用这个新的数据访问类进行工作,而它自己却完全不知道。

2.3. Spring IoC 实战应用

2.3.1. 示例 1

考虑以下类:

package istia.st.springioc.domain;

public class Personne {
  private String nom;
  private int age;

   // 显示“Personne”
  public String toString() {
    return "nom=[" + this.nom + "], age=[" + this.age + "]";
  }

   // 初始化-关闭
  public void init() {
    System.out.println("init personne [" + this.toString() + "]");
  }

  public void close() {
    System.out.println("destroy personne [" + this.toString() + "]");
  }

   // getter-setter
  public int getAge() {
    return age;
  }

  public void setAge(int age) {
    this.age = age;
  }

  public String getNom() {
    return nom;
  }

  public void setNom(String nom) {
    this.nom = nom;
  }
}

该类包含:

  • 两个私有字段 nom age
  • 这两个字段的读取(get)和写入(set)方法
  • 一个方法 toString,用于将对象 [Personne] 的值以字符串形式返回
  • 一个 init 方法,将在 Spring 创建对象时被调用;一个 close 方法,将在对象销毁时被调用

要创建 [Personne] 类型的对象,我们将使用以下 Spring 文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="personne1" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Simon</value>
        </property>
        <property name="age">
            <value>40</value>
        </property>
    </bean>
    <bean id="personne2" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Brigitte</value>
        </property>
        <property name="age">
            <value>20</value>
        </property>
    </bean>
</beans>

该文件将命名为 config.xml

  • 它定义了两个键分别为“personne1”和“personne2”、类型为[Personne]的Bean
  • 它初始化了每个人的 [nom, age] 字段
  • 它定义了在初始化对象 [init-method] 时以及销毁对象 [destroy-method] 时需要调用的方法

在测试中,我们将使用一个名为 JUnit 的测试类,并逐步向其中添加方法。该类的初始版本如下:

package istia.st.springioc.tests;

import istia.st.springioc.domain.Personne;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import junit.framework.TestCase;

public class Tests extends TestCase {

   // Bean工厂
  private ListableBeanFactory bf;

   // 初始化测试
  public void setUp() {
    bf = new XmlBeanFactory(new ClassPathResource("config.xml"));
  }

  public void test1() {
    // 通过键从 Spring 文件中检索 Bean [Personne]
    Personne personne1 = (Personne) bf.getBean("personne1");
    System.out.println("personne1=" + personne1.toString());
    Personne personne2 = (Personne) bf.getBean("personne2");
    System.out.println("personne2=" + personne2.toString());
    personne2 = (Personne) bf.getBean("personne2");
    System.out.println("personne2=" + personne2.toString());
  }
}

注释:

  • 为了获取在文件 [config.xml] 中定义的 Bean,我们使用类型为 [ListableBeanFactory] 的对象。 还有其他类型的对象可用于访问 Bean。[ListableBeanFactory] 对象是在测试类的 [setUp] 方法中获取的,并存储在一个私有变量中。这样,所有测试方法都可以使用它。
  • 文件 [config.xml] 将被放置在应用程序 [ClassPath] 的 c.a.d 中,该目录是 Java 虚拟机在查找应用程序引用的类时会搜索的目录之一。 对象 [ClassPathResource] 用于在应用程序的 [ClassPath] 中查找资源,此处即文件 [config.xml]。
  • Spring 可以使用多种格式的配置文件。[XmlBeanFactory] 对象用于解析 XML 格式的配置文件。
  • 解析 Spring 文件会生成一个 [ListableBeanFactory] 类型的对象,此处为对象 bf。利用该对象,可通过 bf.getBean(C) 获取由键 C 标识的 Bean。
  • 方法 [test1] 用于获取并显示键为 "personne1" 和 "personne2" 的 Bean 的值。

我们应用程序的 Eclipse 项目结构如下:

Image

注释:

  • 文件夹 [src] 包含源代码。编译后的代码将存放在未在此处显示的文件夹 [bin] 中。
  • 文件 [config.xml] 位于文件夹 [src] 的根目录下。 项目构建时会自动将其复制到 [bin] 文件夹中,该文件夹属于应用程序的 [ClassPath] 目录。[ClassPathResource] 对象会在此处查找该文件。
  • [lib]文件夹包含应用程序所需的三个Java库:
      • commons-logging.jar spring-core.jar 用于 Spring 类
      • junit.jar 用于 JUnit 类
  • 文件 [lib] 同样属于应用程序的 [ClassPath]

执行测试 JUnit 中的方法 [test1] 得到以下结果:

18 sept. 2004 11:28:53 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [config.xml]
18 sept. 2004 11:28:53 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne1'
init personne [nom=[Simon], age=[40]]
personne1=nom=[Simon], age=[40]
18 sept. 2004 11:28:53 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne2'
init personne [nom=[Brigitte], age=[20]]
personne2=nom=[Brigitte], age=[20]
personne2=nom=[Brigitte], age=[20]

注释:

  • Spring 通过 [commons-logging.jar] 库记录了一系列事件。这些日志有助于我们更好地理解 Spring 的运行机制。
  • 文件 [config.xml] 已被加载并用于
  • 操作*
Personne personne1 = (Personne) bf.getBean("personne1");

操作强制创建了 Bean [personne1]。我们可以看到 Spring 对此的日志记录。 由于在 [personne1] Bean 的定义中写成了 [init-method="init"],因此执行了所创建的 [Personne] 对象的 [init] 方法。相应的消息已显示。

  • 操作
System.out.println("personne1=" + personne1.toString());

操作显示了所创建的对象 [Personne] 的值。

  • 对于键 Bean [personne2],同样的情况再次发生。
  • 最后一个操作
    personne2 = (Personne) bf.getBean("personne2");
    System.out.println("personne2=" + personne2.toString());

并未导致创建新的 [Personne] 类型对象。如果确实如此,本应显示 [init] 方法,但实际并未发生。这就是单例模式的原理。 Spring默认情况下仅会创建配置文件中Bean的唯一实例。它是一个对象引用服务。如果请求一个尚未创建的对象的引用,它会创建该对象并返回一个引用;如果对象已存在,Spring则直接返回该对象的引用。

  • 可以注意到,对象 [Personne] 的方法 [close] 完全不见踪影,尽管我们在 [destroy-method=close] 的 Bean 定义中写过它。 可能是该方法仅在垃圾回收器回收该对象占用的内存时才会被执行。此时应用程序已结束运行,因此向屏幕输出内容已无实际意义。需进一步验证。

既然已经掌握了 Spring 配置的基础知识,接下来的讲解将会更加简洁明了。

2.3.2. 示例 2

考虑以下新的类 [Voiture]:

package istia.st.springioc.domain;

public class Voiture {
  private String marque;
  private String type;
  private Personne propriétaire;

   // 构造函数

  public Voiture() {
  }

  public Voiture(String marque, String type, Personne propriétaire) {
    this.marque = marque;
    this.type = type;
    this.propriétaire = propriétaire;
  }

   // toString
  public String toString() {
    return "Voiture : marque=[" + this.marque + "] type=[" + this.type
        + "] propriétaire=[" + this.propriétaire + "]";
  }

     // getter-setter
  public String getMarque() {
    return marque;
  }

  public void setMarque(String marque) {
    this.marque = marque;
  }

  public Personne getPropriétaire() {
    return propriétaire;
  }

  public void setPropriétaire(Personne propriétaire) {
    this.propriétaire = propriétaire;
  }

  public String getType() {
    return type;
  }

  public void setType(String type) {
    this.type = type;
  }

   // 初始化-关闭
  public void init() {
    System.out.println("init voiture [" + this.toString() + "]");
  }

  public void close() {
    System.out.println("destroy voiture [" + this.toString() + "]");
  }

}

该类包含:

  • 三个私有字段:typemarque propriétaire。这些字段可以通过 bean 的 get set 公共方法进行初始化和读取。它们也可以通过构造函数 Voiture(String, String, Personne) 进行初始化。该类还拥有一个无参构造函数,以符合 JavaBean 规范。
  • 一个 toString 方法,用于以字符串形式获取 [Voiture] 对象的值
  • 一个 init 方法,将在对象创建后立即由 Spring 调用;一个 close 方法,将在对象销毁时调用

要创建 [Voiture] 类型的对象,我们将使用以下 Spring 文件 [config.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="personne1" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Simon</value>
        </property>
        <property name="age">
            <value>40</value>
        </property>
    </bean>
    <bean id="personne2" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Brigitte</value>
        </property>
        <property name="age">
            <value>20</value>
        </property>
    </bean>
    <bean id="voiture1" class="istia.st.springioc.domain.Voiture" 
        init-method="init" destroy-method="close">
        <constructor-arg index="0">
            <value>Peugeot</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>307</value>
        </constructor-arg>
        <constructor-arg index="2">
            <ref bean="personne2"></ref>
        </constructor-arg>
    </bean>
</beans>

该文件在之前的定义基础上,新增了一个键为“voiture1”、类型为[Voiture]的Bean。要初始化该Bean,我们可以这样编写:

    <bean id="voiture1" class="istia.st.springioc.domain.Voiture" 
        init-method="init" destroy-method="close">
        <property name="marque">
            <value>Peugeot</value>
        </property>
        <property name="type">
            <value>307</value>
        </property>
        <property name="propriétaire">
            <ref bean="personne2"/>
        </property>
    </bean>

与其采用前面介绍的方法,我们在此选择使用该类的构造函数 Voiture(String, String, Personne)。 此外,Bean [voiture1] 定义了在初始化 [init-method] 对象时需调用的方法,以及在销毁 [destroy-method] 对象时需调用的方法。

在测试中,我们将使用之前介绍过的测试类 JUnit,并为其添加以下方法 [test2]:

1
2
3
4
5
  public void test2() {
     // bean 检索[voiture1]
    Voiture Voiture1 = (Voiture) bf.getBean("voiture1");
    System.out.println("Voiture1=" + Voiture1.toString());
  }

方法 [test2] 获取 Bean [voiture1] 并将其显示出来。

Eclipse 项目的结构与前一个测试中保持一致。执行测试 JUnit 中的方法 [test2] 会得到以下结果:

18 sept. 2004 14:56:10 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [config.xml]
18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'voiture1'
18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne2'
init personne [nom=[Brigitte], age=[20]]
18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'voiture1' instantiated via constructor [public istia.st.springioc.domain.Voiture(java.lang.String,java.lang.String,istia.st.springioc.domain.Personne)]
init voiture [Voiture : marque=[Peugeot] type=[307] propriétaire=[nom=[Brigitte], age=[20]]]
Voiture1=Voiture : marque=[Peugeot] type=[307] propriétaire=[nom=[Brigitte], age=[20]]

注释:

  1. 方法 [test2] 请求对 Bean [voiture1] 的引用
  2. 第 4 行:Spring 开始创建 Bean [voiture1],因为该 Bean 尚未创建(单例)
  3. 第 6 行:由于 Bean [voiture1] 引用了 Bean [personne2],因此后者也被构建
  4. 第 7 行:bean [personne2] 已创建。随后执行其方法 [init]。
  5. 第 9 行:Spring 表示将使用构造函数来创建 Bean [voiture1]
  6. 第 10 行:已创建 Bean [voiture1]。随后执行其方法 [init]。
  7. 第 11 行:方法 [test2] 显示 Bean [voiture1] 的值

2.3.3. 示例 3

我们引入以下新的 [GroupePersonnes] 类:

package istia.st.springioc.domain;

import java.util.Map;

public class GroupePersonnes {
  private Personne[] membres;
  private Map groupesDeTravail;

   // getter - setter
  public Personne[] getMembres() {
    return membres;
  }

  public void setMembres(Personne[] membres) {
    this.membres = membres;
  }

  public Map getGroupesDeTravail() {
    return groupesDeTravail;
  }

  public void setGroupesDeTravail(Map groupesDeTravail) {
    this.groupesDeTravail = groupesDeTravail;
  }

   // 显示
  public String toString() {
    String liste = "membres : ";
    for (int i = 0; i < this.membres.length; i++) {
      liste += "[" + this.membres[i].toString() + "]";
    }
    return liste + ", groupes de travail = " + this.groupesDeTravail.toString();
  }

   // 初始化-关闭
  public void init() {
    System.out.println("init GroupePersonnes [" + this.toString() + "]");
  }

  public void close() {
    System.out.println("destroy GroupePersonnes [" + this.toString() + "]");
  }
}

其两个私有成员为:

成员:一个包含该组成员的数组

groupesDeTravail:一个将人员分配到工作组的字典

需要注意的是,类 [GroupePersonnes] 并未定义无参构造器,以遵循 JavaBean 规范。需要提醒的是,如果完全没有构造器,则会存在一个“默认”构造器,即无参构造器,它不执行任何操作。

本文旨在展示 Spring 如何初始化复杂对象,例如包含数组或字典类型字段的对象。我们在前面的 Spring 文件 [config.xml] 中添加一个新的 Bean:

    <bean id="groupe1" class="istia.st.springioc.domain.GroupePersonnes" 
        init-method="init" destroy-method="close">
        <property name="membres">
            <list>
                <ref bean="personne1"/>
                <ref bean="personne2"/>
            </list>
        </property>
        <property name="groupesDeTravail">
            <map>
                <entry key="Brigitte">
                    <value>Marketing</value>
                </entry>
                <entry key="Simon">
                    <value>Ressources humaines</value>
                </entry>
            </map>
        </property>
    </bean>
  1. <list> 标签可用于为数组类型或实现 List 接口的字段初始化不同值。
  2. <map> 标签可用于对实现 Map 接口的字段执行相同操作

在测试中,我们将使用之前介绍过的测试类 JUnit,并为其添加以下方法 [test3]:

1
2
3
4
5
  public void test3() {
    // 获取 Bean [groupe1]
    GroupePersonnes groupe1 = (GroupePersonnes) bf.getBean("groupe1");
    System.out.println("groupe1=" + groupe1.toString());
  }

方法 [test3] 获取 Bean [groupe1] 并将其显示出来。

Eclipse 项目的结构与前一个测试中保持一致。执行测试 JUnit 中的方法 [test3] 会得到以下结果:

18 sept. 2004 15:51:45 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [config.xml]
18 sept. 2004 15:51:45 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'groupe1'
18 sept. 2004 15:51:45 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne1'
init personne [nom=[Simon], age=[40]]
18 sept. 2004 15:51:45 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne2'
init personne [nom=[Brigitte], age=[20]]
init GroupePersonnes [membres : [nom=[Simon], age=[40]][nom=[Brigitte], age=[20]], groupes de travail = {Brigitte=Marketing, Simon=Ressources humaines}]
groupe1=membres : [nom=[Simon], age=[40]][nom=[Brigitte], age=[20]], groupes de travail = {Brigitte=Marketing, Simon=Ressources humaines}

注释:

  • 方法 [test3] 请求 bean [groupe1] 的引用
  • 第 4 行:Spring 开始创建该 Bean
  • 由于 Bean [groupe1] 引用了 Bean [personne1] 和 [personne2],因此这两个 Bean 被创建(第 6 行和第 9 行),并执行了它们的 init 方法(第 7 行和第 10 行)
  • 第 11 行:bean [groupe1] 已创建。现在执行其方法 [init]。
  • 第 12 行:由方法 [test3] 请求显示。

2.4. 使用 Spring 配置三层 Web 应用程序

2.4.1. 应用程序的总体架构

我们希望构建一个具有以下结构的三层应用程序:

  • 通过使用 Java 接口,使三层相互独立
  • 三层的集成将由 Spring 实现
  • 将为三个层分别创建独立的包,分别命名为ControlDomain和Dao。另外将有一个包用于存放测试应用程序。

在Eclipse中的应用程序结构可能如下所示:

Image

2.4.2. 数据访问层 DAO

DAO层将实现以下接口:

package istia.st.demo.dao;

public interface IDao1 {
  public int doSometingInDaoLayer(int a, int b);
}
  • 编写两个类 Dao1Impl1 Dao1Impl2,它们实现接口 IDao1。 方法 Dao1Impl1.doSomethingInDaoLayer 的返回值为 a+b,而方法 Dao1Impl2.doSomethingInDaoLayer 的返回值为 a-b。
  • 编写一个测试类 JUnit 来测试前两个类

2.4.3. 业务层

业务层将实现以下接口:

package istia.st.demo.domain;

public interface IDomain1 {
  public int doSomethingInDomainLayer(int a, int b);
}
  • 编写两个类 Domain1Impl1 Domain1Impl2,它们实现接口 IDomain1。这些类将有一个构造函数,其参数类型为 IDao1。 方法 Domain1Impl1.doSomethingInDomainLayer 将 a 和 b 各增加 1,然后将这两个参数传递给接收到的 IDao1 类型对象上的方法 doSomethingInDaoLayer。 而方法 Domain1Impl2.doSomethingInDomainLayer 则会先将 a 和 b 减去 1,然后执行相同的操作。
  • 编写一个测试类 JUnit 来测试前两个类

2.4.4. 用户界面层

用户界面层将实现以下接口:

package istia.st.demo.control;

public interface IControl1 {
  public int doSometingInControlLayer(int a, int b);
}
  • 编写两个类 Control1Impl1 Control1Impl2,它们实现接口 IControl1。这些类将有一个构造函数,该构造函数接收一个类型为 IDomain1 的参数。 方法 Control1Impl1.doSomethingInControlLayer 将 a 和 b 各增加 1,然后将这两个参数传递给接收到的 IDomain1 类型对象上的方法 doSomethingInDomainLayer。 而方法 Control11Impl2.doSomethingInControlLayer 则会先将 a 和 b 减去 1,然后执行相同的操作。
  • 编写一个测试类 JUnit 来测试前两个类

2.4.5. 与 Spring 的集成

  • 编写一个 Spring 配置文件,用于决定前三个层级各自应使用哪些类
  • 编写测试类 JUnit,使用不同的 Spring 配置,以突出所编写应用程序的灵活性
  • 编写一个独立应用程序(main方法),向接口IControl1传递两个参数,并显示接口渲染的结果。

2.4.6. 解决方案

2.4.6.1. Eclipse 项目

Image

[lib] 文件夹的存档已添加到项目的 [ClassPath] 中。

2.4.6.2. [istia.st.demo.dao] 包

界面:

1
2
3
4
5
6
7
8
9
package istia.st.demo.dao;

/**
 * @author ST-ISTIA
 *  
 */
public interface IDao1 {
  public int doSometingInDaoLayer(int a, int b);
}

第一个实现类:

package istia.st.demo.dao;

/**
 * @author ST-ISTIA
 *  
 */
public class Dao1Impl1 implements IDao1 {

     // 在 [dao] 层中执行某些操作
  public int doSometingInDaoLayer(int a, int b) {
    return a+b;
  }

}

第二个实现类:

package istia.st.demo.dao;

/**
 * @author ST-ISTIA
 *
 */
public class Dao1Impl2 implements IDao1 {

     // 在 [dao] 层中执行某些操作
  public int doSometingInDaoLayer(int a, int b) {
    return a-b;
  }
}

2.4.6.3. [istia.st.demo.domain] 软件包

接口:

package istia.st.demo.domain;

/**
 * @author ST-ISTIA
 *  
 */
public interface IDomain1 {

     // 在 [domain] 层中执行某些操作
  public int doSomethingInDomainLayer(int a, int b);
}

第一个实现类:

package istia.st.demo.domain;

import istia.st.demo.dao.IDao1;

/**
 * @author ST-ISTIA
 *  
 */
public class Domain1Impl1 implements IDomain1 {

     // 访问 [dao] 层的服务
  private IDao1 dao1;

  public Domain1Impl1() {
     // 无参数构造函数
  }

     // 存储层访问服务 [dao]
  public Domain1Impl1(IDao1 dao1) {
    this.dao1 = dao1;
  }

     // 在 [domain] 层中执行某些操作
  public int doSomethingInDomainLayer(int a, int b) {
    a++;
    b++;
    return dao1.doSometingInDaoLayer(a, b);
  }
}

第二个实现类:

package istia.st.demo.domain;

import istia.st.demo.dao.IDao1;

/**
 * @author ST-ISTIA
 *  
 */
public class Domain1Impl2 implements IDomain1 {

     // [dao] 层访问服务
  private IDao1 dao1;

  public Domain1Impl2() {
     // 无参数构造函数
  }

     // 存储层访问服务 [dao]
  public Domain1Impl2(IDao1 dao1) {
    this.dao1 = dao1;
  }

     // 在 [domain] 层中执行某些操作
  public int doSomethingInDomainLayer(int a, int b) {
    a--;
    b--;
    return dao1.doSometingInDaoLayer(a, b);
  }
}

2.4.6.4. [istia.st.demo.control] 包

接口

1
2
3
4
5
6
7
8
9
package istia.st.demo.control;

/**
 * @author ST-ISTIA
 *  
 */
public interface IControl1 {
  public int doSometingInControlLayer(int a, int b);
}

第一个实现类:

package istia.st.demo.control;

import istia.st.demo.domain.IDomain1;

/**
 * @author ST-ISTIA
 *  
 */
public class Control1Impl1 implements IControl1 {
   // [domain] 层中的业务类
    private IDomain1 domain1;

  public Control1Impl1() {
     // 无参构造函数
  }

     // [domain] 层的访问服务实例化
  public Control1Impl1(IDomain1 domain1) {
    this.domain1 = domain1;
  }

     // 执行某些操作
  public int doSometingInControlLayer(int a, int b) {
    a++;
    b++;
    return domain1.doSomethingInDomainLayer(a, b);
  }

}

第二个实现类:

package istia.st.demo.control;

import istia.st.demo.domain.IDomain1;

/**
 * @author ST-ISTIA
 *  
 */
public class Control1Impl2 implements IControl1 {

     // [domain] 层访问类
    private IDomain1 domain1;

  public Control1Impl2() {
     // 无参构造函数
  }

     // 存储访问层的类 [domain]
  public Control1Impl2(IDomain1 domain1) {
    this.domain1 = domain1;
  }

     // 执行某些操作
  public int doSometingInControlLayer(int a, int b) {
    a--;
    b--;
    return domain1.doSomethingInDomainLayer(a, b);
  }

}

2.4.6.5. 配置文件 [Spring]

第一个 [springMainTest1.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- DAO类 -->
    <bean id="dao" class="istia.st.demo.dao.Dao1Impl1">
    </bean>
     <!-- 业务类 -->
    <bean id="domain" class="istia.st.demo.domain.Domain1Impl1">
        <constructor-arg index="0">
            <ref bean="dao"/>
        </constructor-arg>
    </bean>
     <!-- 控制类 -->
    <bean id="control" class="istia.st.demo.control.Control1Impl1">
        <constructor-arg index="0">
            <ref bean="domain"/>
        </constructor-arg>
    </bean>
</beans>

第二个 [springMainTest2.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- DAO类 -->
    <bean id="dao" class="istia.st.demo.dao.Dao1Impl2">
    </bean>
     <!-- 业务类 -->
    <bean id="domain" class="istia.st.demo.domain.Domain1Impl2">
        <constructor-arg index="0">
            <ref bean="dao"/>
        </constructor-arg>
    </bean>
     <!-- 控制类 -->
    <bean id="control" class="istia.st.demo.control.Control1Impl2">
        <constructor-arg index="0">
            <ref bean="domain"/>
        </constructor-arg>
    </bean>
</beans>

2.4.6.6. 测试包 [istia.st.demo.tests]

一个 [main] 类型的测试:

package istia.st.demo.tests;

import istia.st.demo.control.IControl1;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

/**
 * @author ST-ISTIA
 *  
 */
public class MainTest1 {
  public static void main(String[] arguments) {
     // 获取接口的实现IControl1
    IControl1 control = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest1.xml"))).getBean("control");
     // 使用该类
    int a = 10, b = 20;
    int res = control.doSometingInControlLayer(a, b);
     // 显示结果
    System.out.println("control(" + a + "," + b + ")=" + res);
  }
}

在 Eclipse 控制台上获得的结果:

11 mars 2005 11:25:14 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [springMainTest1.xml]
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'control'
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'domain'
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'dao'
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'domain' instantiated via constructor [public istia.st.demo.domain.Domain1Impl1(istia.st.demo.dao.IDao1)]
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'control' instantiated via constructor [public istia.st.demo.control.Control1Impl1(istia.st.demo.domain.IDomain1)]
control(10,20)=34

使用第二个配置文件的另一个测试 [Spring]:

package istia.st.demo.tests;

import istia.st.demo.control.IControl1;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

/**
 * @author ST-ISTIA
 *  
 */
public class MainTest2 {
  public static void main(String[] arguments) {
     // 获取接口的实现IControl1
    IControl1 control = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest2.xml"))).getBean("control");
     // 使用该类
    int a = 10, b = 20;
    int res = control.doSometingInControlLayer(a, b);
     // 显示结果
    System.out.println("control(" + a + "," + b + ")=" + res);
  }
}

在 Eclipse 控制台中获得的结果:

11 mars 2005 11:28:52 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [springMainTest2.xml]
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'control'
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'domain'
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'dao'
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'domain' instantiated via constructor [public istia.st.demo.domain.Domain1Impl2(istia.st.demo.dao.IDao1)]
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'control' instantiated via constructor [public istia.st.demo.control.Control1Impl2(istia.st.demo.domain.IDomain1)]
control(10,20)=-10

最后,一个JUnit测试:

package istia.st.demo.tests;

import istia.st.demo.control.IControl1;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import junit.framework.TestCase;

/**
 * @author ST-ISTIA
 *  
 */
public class JunitTest2Control1 extends TestCase {
  public void testControl1() {
     // 获取接口 IControl1 的实现
    IControl1 control1 = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest1.xml"))).getBean("control");
     // 使用该类
    int a1 = 10, b1 = 20;
    int res1 = control1.doSometingInControlLayer(a1, b1);
    assertEquals(34, res1);
     // 获取接口的另一个实现 IControl1
    IControl1 control2 = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest2.xml"))).getBean("control");
     // 使用类
    int a2 = 10, b2 = 20;
    int res2 = control2.doSometingInControlLayer(a2, b2);
    assertEquals(-10, res2);
  }
}

2.5. Conclusion

Spring 框架在应用程序架构和配置方面都提供了真正的灵活性。我们使用了 IoC 概念,这是 Spring 的两大支柱之一。 另一个支柱是 AOP(面向切面编程),我们尚未介绍。它允许通过配置为类的方法添加“行为”,而无需修改该方法的代码。简而言之,AOP 可以过滤对某些方法的调用:

  • 过滤器可以在目标 M 方法之前、之后或两者都执行。
  • M方法会忽略这些过滤器的存在。这些过滤器在Spring配置文件中定义。
  • M方法的代码不会被修改。过滤器是需要构建的Java类。Spring提供了预定义的过滤器,特别是用于管理SGBD事务的过滤器。
  • 过滤器是 Bean,因此作为 Bean 在 Spring 配置文件中进行定义。

一种常见的过滤器是事务过滤器。 假设业务层有一个方法 M,该方法对数据执行两项不可分割的操作(工作单元)。它调用 DAO 层中的两个方法 M1 和 M2 来完成这两项操作。

由于位于业务层,方法 M 忽略了这些数据的存储介质。例如,它无需 需要假设数据位于 SGBD 中,并将其对 M1 和 M2 方法的两次调用置于 SGBD 的事务中。 这些细节应由 DAO 层负责处理。 因此,解决上述问题的方案是在 DAO 层创建一个方法,该方法将自行调用 M1 和 M2 方法, 并将这些调用封装在 SGBD 的事务中。

AOP 的过滤方案更为灵活。它允许定义一个过滤器,该过滤器将在调用 M 之前启动一个事务,并在调用结束后根据具体情况执行提交或回滚。

这种方法有以下几个优点:

  • 一旦定义了过滤器,即可将其应用于多个方法,例如所有需要事务的方法
  • 经过筛选的方法无需重写
  • 由于要使用的过滤器通过配置定义,因此可以随时更改

除了 IoC 和 AOP 这些概念外,Spring 还为三层应用提供了许多支持类:

  • 在 DAO 层中,针对 JDBC 的 SqlMap(iBatis)、Hibernate 的 JDO(Java Data Object)
  • 在用户界面层中,MVC 用于模型

更多信息请访问:http://www.springframework.org。