文章目录

一、Java Spring框架是什么?它有哪些好处?

Spring 是分层的 Java SE/EE full-stack 轻量级开源框架,以 IoC(Inverse of Control,控制反转)和 AOP(Aspect Oriented Programming,面向切面编程)为内核,使用基本的 JavaBean 完成以前只可能由 EJB 完成的工作,取代了 EJB 臃肿和低效的开发模式。

Spring 具有简单、可测试和松耦合等特点,不仅可以用于服务器端的开发,也可以应用于任何 Java 应用的开发中。Spring 框架的主要优点具体如下。

1)方便解耦,简化开发

Spring 就是一个大工厂,可以将所有对象的创建和依赖关系的维护交给 Spring 管理。

2)方便集成各种优秀框架

Spring 不排斥各种优秀的开源框架,其内部提供了对各种优秀框架(如 Struts2、Hibernate、MyBatis 等)的直接支持。

3)降低 Java EE API 的使用难度

Spring 对 Java EE 开发中非常难用的一些 API(JDBC、JavaMail、远程调用等)都提供了封装,使这些 API 应用的难度大大降低。

4)方便程序的测试

Spring 支持 JUnit4,可以通过注解方便地测试 Spring 程序。

5)AOP 编程的支持

Spring 提供面向切面编程,可以方便地实现对程序进行权限拦截和运行监控等功能。

6)声明式事务的支持

只需要通过配置就可以完成对事务的管理,而无须手动编程。

二、Spring体系结构详解

Spring 框架采用分层架构,根据不同的功能被划分成了多个模块,这些模块大体可分为 Data Access/Integration、Web、AOP、Aspects、Messaging、Instrumentation、Core Container 和 Test,如图所示。

Spring的体系结构
图 中包含了 Spring 框架的所有模块,这些模块可以满足一切企业级应用开发的需求,在开发过程中可以根据需求有选择性地使用所需要的模块。下面分别对这些模块的作用进行简单介绍。

  1. Data Access/Integration(数据访问/集成)

数据访问/集成层包括 JDBC、ORM、OXM、JMS 和 Transactions 模块,具体介绍如下。

  • JDBC 模块:提供了一个 JDBC 的抽象层,大幅度减少了在开发过程中对数据库操作的编码。
  • ORM 模块:对流行的对象关系映射 API,包括 JPA、JDO、Hibernate和 iBatis 提供了的集成层。
  • OXM 模块:提供了一个支持对象/XML 映射的抽象层实现,如 JAXB、Castor、XMLBeans、JiBX 和 XStream。
  • JMS 模块:指 Java 消息服务,包含的功能为生产和消费的信息。
  • Transactions 事务模块:支持编程和声明式事务管理实现特殊接口类,并为所有的 POJO。
  1. Web 模块

Spring 的 Web 层包括 Web、Servlet、Struts 和 Portlet 组件,具体介绍如下。

  • Web 模块:提供了基本的 Web 开发集成特性,例如多文件上传功能、使用的 Servlet 监听器的 IoC 容器初始化以及 Web 应用上下文。
  • Servlet模块:包括 Spring 模型—视图—控制器(MVC)实现 Web 应用程序。
  • Struts 模块:包含支持类内的 Spring 应用程序,集成了经典的 Struts Web 层。
  • Portlet 模块:提供了在 Portlet 环境中使用 MV C实现,类似 Web-Servlet 模块的功能。
  1. Core Container(核心容器)

Spring 的核心容器是其他模块建立的基础,由 Beans 模块、Core 核心模块、Context 上下文模块和 Expression Language 表达式语言模块组成,具体介绍如下。

  • Beans 模块:提供了 BeanFactory,是工厂模式的经典实现,Spring 将管理对象称为 Bean。
  • Core 核心模块:提供了 Spring 框架的基本组成部分,包括 IoC 和 DI 功能。
  • Context 上下文模块:建立在核心和 Beans 模块的基础之上,它是访问定义和配置任何对象的媒介。ApplicationContext 接口是上下文模块的焦点。
  • Expression Language 模块:是运行时查询和操作对象图的强大的表达式语言。
  1. 其他模块

Spring的其他模块还有 AOP、Aspects、Instrumentation 以及 Test 模块,具体介绍如下。

  • AOP 模块:提供了面向切面编程实现,允许定义方法拦截器和切入点,将代码按照功能进行分离,以降低耦合性。
  • Aspects 模块:提供与 AspectJ 的集成,是一个功能强大且成熟的面向切面编程(AOP)框架。
  • Instrumentation 模块:提供了类工具的支持和类加载器的实现,可以在特定的应用服务器中使用。
  • Test 模块:支持 Spring 组件,使用 JUnit 或 TestNG 框架的测试。

三、Spring目录结构和基础JAR包介绍

目前 Spring框架的最新版本是 5.1.8,本教程是基于 Spring 的稳定版本 3.2.13 进行讲解的。读者可以通过网址 http://repo.spring.io/simple/libs-release-local/org/springframework/spring/ 下载名称为 springframework-3.2.13.RELEASE-dist.zip 的压缩包。在浏览器的地址栏中输入此下载地址后,浏览器的访问结果如图所示。

访问结果

从图中找到所需要的 Spring 框架压缩包。单击此链接下载,下载完成后,解压文件的目录结构如图所示。

解压后目录

下面对上图所示的目录进行简单介绍,具体如表 1 所示。

名称作用
docs包含 Spring 的 API 文档和开发规范
libs包含开发需要的 JAR 包和源码包
schema包含开发所需要的 schema 文件,在这些文件中定义了 Spring 相关配置文件的约束

在 libs 目录中,包含了 Spring 框架提供的所有 JAR 文件,其中有四个 JAR 文件是 Spring 框架的基础包,分别对应 Spring 容器的四个模块,具体如表 2 所示。

名称作用
spring-core-3.2.13.RELEASE.jar包含 Spring 框架基本的核心工具类,Spring 其他组件都要用到这个包中的类,是其他组件的基本核心。
spring-beans-3.2.13.RELEASE.jar所有应用都要用到的,它包含访问配置文件、创建和管理 bean 以及进行 Inversion of Control(IoC)或者 Dependency Injection(DI)操作相关的所有类。
spring-context-3.2.13.RELEASE.jarSpring 提供在基础 IoC 功能上的扩展服务,此外还提供许多企业级服务的支持,如邮件服务、任务调度、JNDI 定位、EJB 集成、远程访问、缓存以及各种视图层框架的封装等
spring-expression-3.2.13.RELEASE.jar定义了 Spring 的表达式语言。 需要注意的是,在使用 Spring 开发时,除了 Spring 自带的 JAR 包以外,还需要一个第三方 JAR 包 commons.logging 处理日志信息

读者可以通过网址 http://commons.apache.org/proper/commons-logging/download_logging.cgi 下载。该 JAR 包现在最新版本为 commons-logging.1.2,下载完成后,解压即可找到。

使用 Spring 框架时,只需将 Spring 的四个基础包以及 commons-logging-1.2.jar 包复制到项目的 lib 目录,并发布到类路径中即可。

四、Spring IoC容器

IoC 是指在程序开发中,实例的创建不再由调用者管理,而是由 Spring 容器创建。Spring 容器会负责控制程序之间的关系,而不是由程序代码直接控制,因此,控制权由程序代码转移到了 Spring 容器中,控制权发生了反转,这就是 Spring 的 IoC 思想。

Spring 提供了两种 IoC 容器,分别为 BeanFactory 和 ApplicationContext,接下来将针对这两种 IoC 容器进行详细讲解。

BeanFactory

BeanFactory 是基础类型的 IoC 容器,它由 org.springframework.beans.facytory.BeanFactory 接口定义,并提供了完整的 IoC 服务支持。简单来说,BeanFactory 就是一个管理 Bean 的工厂,它主要负责初始化各种 Bean,并调用它们的生命周期方法。

BeanFactory 接口有多个实现类,最常见的是 org.springframework.beans.factory.xml.XmlBeanFactory,它是根据 XML 配置文件中的定义装配 Bean 的。

创建 BeanFactory 实例时,需要提供 Spring 所管理容器的详细配置信息,这些信息通常采用 XML 文件形式管理。其加载配置信息的代码具体如下所示:

BeanFactory beanFactory = new XmlBeanFactory(new FileSystemResource("D://applicationContext.xml"));

ApplicationContext

ApplicationContext 是 BeanFactory 的子接口,也被称为应用上下文。 该接口的全路径为 org.springframework.context.ApplicationContext,它不仅提供了 BeanFactory 的所有功能,还添加了对 i18n(国际化)、资源访问、事件传播等方面的良好支持。

ApplicationContext 接口有两个常用的实现类,具体如下。

1)ClassPathXmlApplicationContext

该类从类路径 ClassPath 中寻找指定的 XML 配置文件,找到并装载完成 ApplicationContext 的实例化工作,具体如下所示。

ApplicationContext applicationContext = new ClassPathXmlApplicationContext(String configLocation);

在上述代码中,configLocation 参数用于指定 Spring 配置文件的名称和位置,如 applicationContext.xml。

2)FileSystemXmlApplicationContext

该类从指定的文件系统路径中寻找指定的 XML 配置文件,找到并装载完成 ApplicationContext 的实例化工作,具体如下所示。

ApplicationContext applicationContext = new FileSystemXmlApplicationContext(String configLocation);

它与 ClassPathXmlApplicationContext 的区别是:在读取 Spring 的配置文件时,FileSystemXmlApplicationContext 不再从类路径中读取配置文件,而是通过参数指定配置文件的位置,它可以获取类路径之外的资源,如“F:/workspaces/applicationContext.xml”。

在使用 Spring 框架时,可以通过实例化其中任何一个类创建 Spring 的 ApplicationContext 容器。

通常在 Java 项目中,会采用通过 ClassPathXmlApplicationContext 类实例化 ApplicationContext 容器的方式,而在 Web 项目中,ApplicationContext 容器的实例化工作会交由 Web 服务器完成。Web 服务器实例化 ApplicationContext 容器通常使用基于 ContextLoaderListener 实现的方式,它只需要在 web.xml 中添加如下代码:

<!--指定Spring配置文件的位置,有多个配置文件时,以逗号分隔-->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <!--spring将加载spring目录下的applicationContext.xml文件-->
    <param-value>
        classpath:spring/applicationContext.xml
    </param-value>
</context-param>
<!--指定以ContextLoaderListener方式启动Spring容器-->
<listener>
    <listener-class>
        org.springframework.web.context.ContextLoaderListener
    </listener-class>
</listener>

需要注意的是,BeanFactory 和 ApplicationContext 都是通过 XML 配置文件加载 Bean 的。

二者的主要区别在于,如果 Bean 的某一个属性没有注入,则使用 BeanFacotry 加载后,在第一次调用 getBean() 方法时会抛出异常,而 ApplicationContext 则在初始化时自检,这样有利于检查所依赖的属性是否注入。

因此,在实际开发中,通常都选择使用 ApplicationContext,而只有在系统资源较少时,才考虑使用 BeanFactory。

五、Spring DI(依赖注入)

依赖注入(Dependency Injection,DI)和控制反转含义相同,它们是从两个角度描述的同一个概念。

当某个 Java 实例需要另一个 Java 实例时,传统的方法是由调用者创建被调用者的实例(例如,使用 new 关键字获得被调用者实例),而使用 Spring 框架后,被调用者的实例不再由调用者创建,而是由 Spring 容器创建,这称为控制反转。

Spring 容器在创建被调用者的实例时,会自动将调用者需要的对象实例注入给调用者,这样,调用者通过 Spring 容器获得被调用者实例,这称为依赖注入。

依赖注入主要有两种实现方式,分别是属性 setter 注入和构造方法注入。具体介绍如下。

1)属性 setter 注入(开发使用)

指 IoC 容器使用 setter 方法注入被依赖的实例。通过调用无参构造器或无参 static 工厂方法实例化 bean 后,调用该 bean 的 setter 方法,即可实现基于 setter 的 DI。

2)构造方法注入

指 IoC 容器使用构造方法注入被依赖的实例。基于构造器的 DI 通过调用带参数的构造方法实现,每个参数代表一个依赖。

下面通过属性 setter 注入的案例演示 Spring 容器是如何实现依赖注入的。具体步骤如下。

1. 创建 PersonService 接口

在 springDemo01 项目的 com.mengma.ioc 包下创建一个名为 PersonService 的接口,该接口中包含一个 addPerson() 方法,如下所示。

package com.mengma.ioc;
public interface PersonService {
    public void addPerson();
}
2. 创建接口实现类 PersonServiceImpl

在 com.mengma.ioc 包下创建一个名为 PersonServiceImpl 的类,该类实现了 PersonService 接口,如下所示。

package com.mengma.ioc;
public class PersonServiceImpl implements PersonService {
    // 定义接口声明
    private PersonDao personDao;
    // 提供set()方法,用于依赖注入
    public void setPersonDao(PersonDao personDao) {
        this.personDao = personDao;
    }
    // 实现PersonService接口的方法
    @Override
    public void addPerson() {
        personDao.add(); // 调用PersonDao中的add()方法
        System.out.println("addPerson()执行了...");
    }
}

上述代码中,首先声明了 personDao 对象,并为其添加 setter 方法,用于依赖注入,然后实现了 PersonDao 接口的 addPerson() 方法,并在方法中调用 save() 方法和输出一条语句。

3. 在 applicationContext.xml 中添加配置信息

在 applicationContext.xml 配置文件中添加一个 元素,用于实例化 PersonServiceImpl 类,并将 personDao 的实例注入到 personService 中,其实现代码如下所示:

<bean id="personService" class="com.mengma.ioc.PersonServiceImpl">
    <!-- 将personDao实例注入personService实例中 -->
    <property name="personDao" ref="personDao"/>
</bean>
4. 编写测试方法

在 FirstTest 类中创建一个名为 test2() 的方法,编辑后如下所示:

@Test
public void test2() {
    // 定义Spring配置文件的路径
    String xmlPath = "applicationContext.xml";
    // 初始化Spring容器,加载配置文件
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
            xmlPath);
    // 通过容器获取personService实例
    PersonService personService = (PersonService) applicationContext
            .getBean("personService");
    // 调用personService的addPerson()方法
    personService.addPerson();
}
5. 运行项目并查看结果

使用 JUnit 测试运行 test2() 方法,运行成功后,控制台的输出结果如图所示。

运行结果
从图的输出结果中可以看出,使用 Spring 容器获取 userService 的实例后,调用了该实例的 addPerson() 方法,在该方法中又调用了 PersonDao 实现类中的 add() 方法,并输出了结果。这就是 Spring 容器属性 setter 注入的方式,也是实际开发中较为常用的一种方式。

六、Spring Bean的配置及常用属性

作为 Spring 核心机制的依赖注入,改变了传统的编程习惯,对组件的实例化不再由应用程序完成,转而交由 Spring 容器完成,在需要时注入应用程序中,从而对组件之间依赖关系进行了解耦。这一切都离不开 Spring 配置文件中使用的 < bean> 元素。

Spring 容器可以被看作一个大工厂,而 Spring 容器中的 Bean 就相当于该工厂的产品。如果希望这个大工厂能够生产和管理 Bean,这时则需要告诉容器需要哪些 Bean,以及需要以何种方式将这些 Bean 装配到一起。

Spring 配置文件支持两种不同的格式,分别是 XML 文件格式和 Properties 文件格式。

通常情况下,Spring 会以 XML 文件格式作为 Spring 的配置文件,这种配置方式通过 XML 文件注册并管理 Bean 之间的依赖关系。

XML 格式配置文件的根元素是 < beans>,该元素包含了多个 < bean> 子元素,每一个 < bean> 子元素定义了一个 Bean,并描述了该 Bean 如何被装配到 Spring 容器中。

定义 Bean 的示例代码如下所示:

<?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:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
    <!-- 使用id属性定义person1,其对应的实现类为com.mengma.person1 -->
    <bean id="person1" class="com.mengma.damain.Person1" />
    <!--使用name属性定义person2,其对应的实现类为com.mengma.domain.Person2-->
    <bean name="Person2" class="com.mengma.domain.Person2"/>
</beans>

在上述代码中,分别使用 id 和 name 属性定义了两个 Bean,并使用 class 元素指定了 Bean 对应的实现类。

< bean> 元素中包含很多属性,其常用属性如表所示。

属性名称描述
id是一个 Bean 的唯一标识符,Spring 容器对 Bean 的配置和管理都通过该属性完成
nameSpring 容器同样可以通过此属性对容器中的 Bean 进行配置和管理,name 属性中可以为 Bean 指定多个名称,每个名称之间用逗号或分号隔开
class该属性指定了 Bean 的具体实现类,它必须是一个完整的类名,使用类的全限定名
scope用于设定 Bean 实例的作用域,其属性值有 singleton(单例)、prototype(原型)、request、session 和 global Session。其默认值是 singleton
constructor-arg< bean>元素的子元素,可以使用此元素传入构造参数进行实例化。该元素的 index 属性指定构造参数的序号(从 0 开始),type 属性指定构造参数的类型
property< bean>元素的子元素,用于调用 Bean 实例中的 Set 方法完成属性赋值,从而完成依赖注入。该元素的 name 属性指定 Bean 实例中的相应属性名
ref< property> 和 < constructor-arg> 等元素的子元索,该元素中的 bean 属性用于指定对 Bean 工厂中某个 Bean 实例的引用
value< property> 和 < constractor-arg> 等元素的子元素,用于直接指定一个常量值
list用于封装 List 或数组类型的依赖注入
set用于封装 Set 类型属性的依赖注入
map用于封装 Map 类型属性的依赖注入
entry< map> 元素的子元素,用于设置一个键值对。其 key 属性指定字符串类型的键值,ref 或 value 子元素指定其值

七、Spring实例化Bean的三种方法

在面向对象的程序中,要想调用某个类的成员方法,就需要先实例化该类的对象。在 Spring 中,实例化 Bean 有三种方式,分别是构造器实例化、静态工厂方式实例化和实例工厂方式实例化。本节将针对这三种方式分别进行讲解。

构造器实例化

构造器实例化是指 Spring 容器通过 Bean 对应的类中默认的构造函数实例化 Bean。下面通过案例演示如何使用构造器实例化 Bean。

1.创建项目并导入 JAR 包

在 Eclipse 中创建一个名称为 springDemo02 的 Web 项目,然后将 Spring 支持和依赖的 JAR 包复制到项目的 lib 目录中,并发布到类路径下

2.创建实体类

在项目的 src 目录下创建一个名为 com.mengma.instance.constructor 的包,在该包下创建一个实体类 Person1,如下所示。

package com.mengma.instance.constructor;
public class Person1 {
}

3.创建 Spring 配置文件

在 com.mengma.instance.constructor 包下创建 Spring 的配置文件 applicationContext.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:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
    <bean id="person1" class="com.mengma.instance.constructor.Person1" />
</beans>

在上述配置中,定义了一个 id 为 person1 的 Bean,其中 class 属性指定了其对应的类为 Person1。

4.创建测试类

在 com.mengma.instance.constructor 包下创建一个名为 InstanceTest1 的测试类,编辑后如下所示。

package com.mengma.instance.constructor;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class InstanceTest1 {
    @Test
    public void test() {
        // 定义Spring配置文件的路径
        String xmlPath = "com/mengma/instance/constructor/ApplicationContext.xml";
        // 初始化Spring容器,加载配置文件,并对bean进行实例化
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
                xmlPath);
        // 通过容器获取id为person1的实例
        System.out.println(applicationContext.getBean("person1"));
    }
}

上述文件中,首先在 test() 方法中定义了 Spring 配置文件的路径,然后 Spring 容器会加载配置文件。在加载的同时,Spring 容器会通过实现类 Person1 中默认的无参构造函数对 Bean 进行实例化。

5.运行程序并查看结果

使用 JUnit 测试运行 test() 方法,运行成功后,控制台的输出结果如图所示。

输出结果

从图的输出结果中可以看出,Spring 容器已经成功对 Bean 进行了实例化,并输出了结果。

静态工厂方式实例化

在 Spring 中,也可以使用静态工厂的方式实例化 Bean。此种方式需要提供一个静态工厂方法创建 Bean 的实例。下面通过案例演示如何使用静态工厂方式实例化 Bean。

1.创建实体类

在项目的 src 目录下创建一个名为 com.mengma.instance.static_factory 的包,并在该包下创建一个实体类 Person2,该类与 Person1 相同,不需要添加任何成员。

2.创建静态工厂类

在 com.mengma.instance.static_factory 包下创建一个名为 MyBeanFactory 的类,并在该类中创建一个名为 createBean() 的静态方法,用于创建 Bean 的实例,如下所示。

package com.mengma.instance.static_factory;
public class MyBeanFactory {
    // 创建Bean实例的静态工厂方法
    public static Person2 createBean() {
        return new Person2();
    }
}

3.创建 Spring 配置文件

在 com.mengma.instance.static_factory 包下创建 Spring 的配置文件 applicationContext.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:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
    <bean id="person2" class="com.mengma.instance.static_factory.MyBeanFactory"
        factory-method="createBean" />
</beans>

上述代码中,定义了一个 id 为 person2 的 Bean,其中 class 属性指定了其对应的工厂实现类为 MyBeanFactory,而 factory-method 属性用于告诉 Spring 容器调用工厂类中的 createBean() 方法获取 Bean 的实例。

4.创建测试类

在 com.mengma.instance.static_factory 包下创建一个名为 InstanceTest2 的测试类,编辑后如下所示。

package com.mengma.instance.static_factory;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class InstanceTest2 {
    @Test
    public void test() {
        // 定义Spring配置文件的路径
        String xmlPath = "com/mengma/instance/static_factory/applicationContext.xml"; // 初始化Spring容器,加载配置文件,并对bean进行实例化
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
                xmlPath);
        // 通过容器获取id为person2实例
        System.out.println(applicationContext.getBean("person2"));
    }
}

5.运行程序并查看结果

使用 JUnit 测试运行 test() 方法,运行成功后,控制台的输出结果如图所示。

从图的输出结果中可以看出,使用静态工厂的方式也成功对 Bean 进行了实例化。

输出结果

实例工厂方式实例化

在 Spring 中,还有一种实例化 Bean 的方式就是采用实例工厂。在这种方式中,工厂类不再使用静态方法创建 Bean 的实例,而是直接在成员方法中创建 Bean 的实例。

同时,在配置文件中,需要实例化的 Bean 也不是通过 class 属性直接指向其实例化的类,而是通过 factory-bean 属性配置一个实例工厂,然后使用 factory-method 属性确定使用工厂中的哪个方法。下面通过案例演示实例工厂方式的使用。

1.创建实体类

在项目的 src 目录下创建一个名为 com.mengma.instance.factory 的包,在该包下创建一个 Person3 类,该类与 Person1 类相同,不需要添加任何成员。

2.创建实例工厂类

在 com.mengma.instance.factory 包下创建一个名为 MyBeanFactory 的类,编辑后如下所示。

package com.mengma.instance.factory;
public class MyBeanFactory {
    public MyBeanFactory() {
        System.out.println("person3工厂实例化中");
    }
    // 创建Bean的方法
    public Person3 createBean() {
        return new Person3();
    }
}

上述代码中,使用默认无参的构造方法输出 person3 工厂实例化中语句,使用 createBean 成员方法创建 Bean 的实例。

3.创建 Spring 配置文件

在 com.mengma.instance.factory 包下创建 Spring 的配置文件 applicationContext.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:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
    <!-- 配置实例工厂 -->
    <bean id="myBeanFactory" class="com.mengma.instance.factory.MyBeanFactory" />
    <!-- factory-bean属性指定一个实例工厂,factory-method属性确定使用工厂中的哪个方法 -->
    <bean id="person3" factory-bean="myBeanFactory" factory-method="createBean" />
</beans>

上述代码中,首先配置了一个实例工厂 Bean,然后配置了需要实例化的 Bean。在 id 为 person3 的 Bean 中,使用 factory-bean 属性指定一个实例工厂,该属性值就是实例工厂的 id 属性值。使用 factory-method 属性确定使用工厂中的 createBean() 方法。

4.创建测试类

在 com.mengma.instance.factory 包下创建一个名为 InstanceTest3 的测试类,编辑后如下所示。

package com.mengma.instance.factory;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class InstanceTest3 {
    @Test
    public void test() {
        // 定义Spring配置文件的路径
        String xmlPath = "com/mengma/instance/factory/applicationContext.xml"; // 初始化Spring容器,加载配置文件,并对bean进行实例化
        // 初始化Spring容器,加载配置文件,并对bean进行实例化
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
                xmlPath);
        // 通过容器获取id为person3实例
        System.out.println(applicationContext.getBean("person3"));
    }
}

5.运行程序并查看结果

使用 JUnit 测试运行 test() 方法,运行成功后,控制台的输出结果如图所示。

输出结果
从图的输出结果中可以看出,使用实例工厂的方式也同样对 Bean 进行了实例化。

八、Spring中Bean的作用域

作用域的种类

Spring 容器在初始化一个 Bean 的实例时,同时会指定该实例的作用域。Spring3 为 Bean 定义了五种作用域,具体如下。

1)singleton

单例模式,使用 singleton 定义的 Bean 在 Spring 容器中只有一个实例,这也是 Bean 默认的作用域。

2)prototype

原型模式,每次通过 Spring 容器获取 prototype 定义的 Bean 时,容器都将创建一个新的 Bean 实例。

3)request

在一次 HTTP 请求中,容器会返回该 Bean 的同一个实例。而对不同的 HTTP 请求,会返回不同的实例,该作用域仅在当前 HTTP Request 内有效。

4)session

在一次 HTTP Session 中,容器会返回该 Bean 的同一个实例。而对不同的 HTTP 请求,会返回不同的实例,该作用域仅在当前 HTTP Session 内有效。

5)global Session

在一个全局的 HTTP Session 中,容器会返回该 Bean 的同一个实例。该作用域仅在使用 portlet context 时有效。

在上述五种作用域中,singleton 和 prototype 是最常用的两种,接下来将对这两种作用域进行详细讲解。

singleton 作用域

singleton 是 Spring 容器默认的作用域,当一个 Bean 的作用域为 singleton 时,Spring 容器中只会存在一个共享的 Bean 实例,并且所有对 Bean 的请求,只要 id 与该 Bean 定义相匹配,就只会返回 Bean 的同一个实例。

通常情况下,这种单例模式对于无会话状态的 Bean(如 DAO 层、Service 层)来说,是最理想的选择。

在 Spring 配置文件中,可以使用 < bean> 元素的 scope 属性,将 Bean 的作用域定义成 singleton,其配置方式如下所示:

< bean id="person" class="com.mengma.scope.Person" scope="singleton"/>

在项目的 src 目录下创建一个名为 com.mengma.scope 的包,在该包下创建 Person 类,类中不需要添加任何成员,然后创建 Spring 的配置文件 applicationContext.xml,将上述 Bean 的定义方式写入配置文件中,最后创建一个名为 PersonTest 的测试类,编辑后如下所示。

package com.mengma.scope;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class PersonTest {
    @Test
    public void test() {
        // 定义Spring配置文件路径
        String xmlPath = "com/mengma/scope/applicationContext.xml";
        // 初始化Spring容器,加载配置文件,并对bean进行实例化
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
                xmlPath);
        // 输出获得实例
        System.out.println(applicationContext.getBean("person"));
        System.out.println(applicationContext.getBean("person"));
    }
}

使用 JUnit 测试运行 test() 方法,运行成功后,控制台的输出结果如图所示。

输出结果
从图中可以看到,两次输出的结果相同,这说明 Spring 容器只创建了一个 Person 类的实例。由于 Spring 容器默认作用域是 singleton,如果不设置 scope=“singleton”,则其输出结果也将是一个实例。

prototype 作用域

使用 prototype 作用域的 Bean 会在每次请求该 Bean 时都会创建一个新的 Bean 实例。因此对需要保持会话状态的 Bean(如 Struts2 的 Action 类)应该使用 prototype 作用域。

在 Spring 配置文件中,要将 Bean 定义为 prototype 作用域,只需将 < bean> 元素的 scope 属性值定义成 prototype,其示例代码如下所示:

< bean id="person" class="com.mengma.scope.Person" scope="prototype"/>

将《singleton作用域》部分中的配置文件更改成上述代码形式后,再次运行 test() 方法,控制台的输出结果如图 2 所示。

输出结果
从图的输出结果中可以看到,两次输出的结果并不相同,这说明在 prototype 作用域下,Spring 容器创建了两个不同的 Person 实例。

九、Spring Bean的生命周期

Spring 容器可以管理 singleton 作用域 Bean 的生命周期,在此作用域下,Spring 能够精确地知道该 Bean 何时被创建,何时初始化完成,以及何时被销毁。

而对于 prototype 作用域的 Bean,Spring 只负责创建,当容器创建了 Bean 的实例后,Bean 的实例就交给客户端代码管理,Spring 容器将不再跟踪其生命周期。每次客户端请求 prototype 作用域的 Bean 时,Spring 容器都会创建一个新的实例,并且不会管那些被配置成 prototype 作用域的 Bean 的生命周期。

了解 Spring 生命周期的意义就在于,可以利用 Bean 在其存活期间的指定时刻完成一些相关操作。这种时刻可能有很多,但一般情况下,会在 Bean 被初始化后和被销毁前执行一些相关操作。

在 Spring 中,Bean 的生命周期是一个很复杂的执行过程,我们可以利用 Spring 提供的方法定制 Bean 的创建过程。

当一个 Bean 被加载到 Spring 容器时,它就具有了生命,而 Spring 容器在保证一个 Bean 能够使用之前,会进行很多工作。Spring 容器中 Bean 的生命周期流程如图所示。

Bean的生命周期
Bean 生命周期的整个执行过程描述如下。

1)根据配置情况调用 Bean 构造方法或工厂方法实例化 Bean。

2)利用依赖注入完成 Bean 中所有属性值的配置注入。

3)如果 Bean 实现了 BeanNameAware 接口,则 Spring 调用 Bean 的 setBeanName() 方法传入当前 Bean 的 id 值。

4)如果 Bean 实现了 BeanFactoryAware 接口,则 Spring 调用 setBeanFactory() 方法传入当前工厂实例的引用。

5)如果 Bean 实现了 ApplicationContextAware 接口,则 Spring 调用 setApplicationContext() 方法传入当前 ApplicationContext 实例的引用。

6)如果 BeanPostProcessor 和 Bean 关联,则 Spring 将调用该接口的预初始化方法 postProcessBeforeInitialzation() 对 Bean 进行加工操作,此处非常重要,Spring 的 AOP 就是利用它实现的。

7)如果 Bean 实现了 InitializingBean 接口,则 Spring 将调用 afterPropertiesSet() 方法。

8)如果在配置文件中通过 init-method 属性指定了初始化方法,则调用该初始化方法。

9)如果 BeanPostProcessor 和 Bean 关联,则 Spring 将调用该接口的初始化方法 postProcessAfterInitialization()。此时,Bean 已经可以被应用系统使用了。

10)如果在 < bean> 中指定了该 Bean 的作用范围为 scope=“singleton”,则将该 Bean 放入 Spring IoC 的缓存池中,将触发 Spring 对该 Bean 的生命周期管理;如果在 < bean> 中指定了该 Bean 的作用范围为 scope=“prototype”,则将该 Bean 交给调用者,调用者管理该 Bean 的生命周期,Spring 不再管理该 Bean。

11)如果 Bean 实现了 DisposableBean 接口,则 Spring 会调用 destory() 方法将 Spring 中的 Bean 销毁;如果在配置文件中通过 destory-method 属性指定了 Bean 的销毁方法,则 Spring 将调用该方法对 Bean 进行销毁。

Spring 为 Bean 提供了细致全面的生命周期过程,通过实现特定的接口或 < bean> 的属性设置,都可以对 Bean 的生命周期过程产生影响。虽然可以随意配置 < bean> 的属性,但是建议不要过多地使用 Bean 实现接口,因为这样会导致代码和 Spring 的聚合过于紧密。

简化流程:

image-20200702160749337

十、Spring基于XML/Annotation装配Bean

Spring基于XML装配Bean

Bean 的装配可以理解为依赖关系注入,Bean 的装配方式也就是 Bean 的依赖注入方式。Spring容器支持多种形式的 Bean 的装配方式,如基于 XML 的 Bean 装配、基于 Annotation 的 Bean 装配和自动装配等。

Spring 基于 XML 的装配通常采用两种实现方式,即设值注入(Setter Injection)和构造注入(Constructor Injection)。本节将讲解如何在 XML 配置文件中使用这两种注入方式。

在 Spring 实例化 Bean 的过程中,首先会调用默认的构造方法实例化 Bean 对象,然后通过 Java 的反射机制调用 setXxx() 方法进行属性的注入。因此,设值注入要求一个 Bean 的对应类必须满足以下两点要求。

  • 必须提供一个默认的无参构造方法。
  • 必须为需要注入的属性提供对应的 setter 方法。

使用设值注入时,在 Spring 配置文件中,需要使用 < bean> 元素的子元素 < property> 元素为每个属性注入值。而使用构造注入时,在配置文件中,主要使用 < constructor-arg> 标签定义构造方法的参数,可以使用其 value 属性(或子元素)设置该参数的值。下面通过案例演示基于 XML 方式的 Bean 的装配。

1. 创建 Person 类

在项目 springDemo02 中的 src 目录下,创建一个名称为 com.mengma.assembly 的包,在该包下创建一个 Person 类,如下所示。

package com.mengma.assembly;
public class Person {
    private String name;
    private int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    // 重写toString()方法
    public String toString() {
        return "Person[name=" + name + ",age=" + age + "]";
    }
    // 默认无参的构造方法
    public Person() {
        super();
    }
    // 有参的构造方法
    public Person(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }
}

上述代码中,定义了 name 和 age 两个属性,并为其提供了 getter 和 setter 方法,由于要使用构造注入,所以需要提供有参的构造方法。为了能更清楚地看到输出结果,这里还重写了 toString() 方法。

2. 创建 Spring 配置文件

在 com.mengma.assembly 包下创建一个名为 applicationContext.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:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
    <!-- 使用设值注入方式装配Person实例 -->
    <bean id="person1" class="com.mengma.assembly.Person">
        <property name="name" value="zhangsan" />
        <property name="age" value="20" />
    </bean>
    <!-- 使用构造方法装配Person实例 -->
    <bean id="person2" class="com.mengma.assembly.Person">
        <constructor-arg index="0" value="lisi" />
        <constructor-arg index="1" value="21" />
    </bean>
</beans>

上述代码中,首先使用了设值注入方式装配 Person 类的实例,其中 < property> 子元素用于调用 Bean 实例中的 setXxx() 方法完成属性赋值。然后使用了构造方式装配了 Person 类的实例,其中 < constructor-arg> 元素用于定义构造方法的参数,其属性 index 表示其索引(从 0 开始),value 属性用于设置注入的值。

3. 创建测试类

在 com.mengma.assembly 包下创建一个名称为 XmlBeanAssemblyTest 的测试类,编辑后如下所示。

package com.mengma.assembly;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class XmlBeanAssemblyTest {
    @Test
    public void test() {
        // 定义Spring配置文件路径
        String xmlPath = "com/mengma/assembly/applicationContext.xml";
        // 初始化Spring容器,加载配置文件,并对bean进行实例化
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
                xmlPath);
        // 设值方式输出结果
        System.out.println(applicationContext.getBean("person1"));
        // 构造方式输出结果
        System.out.println(applicationContext.getBean("person2"));
    }
}

上述代码中,分别获取并输出了 id 为 person1 和 person2 的实例。

4. 运行项目并查看结果

使用 JUnit 测试运行 test() 方法,运行成功后,控制台的输出结果如图所示。

输出结果
从图的输出结果中可以看出,使用设值注入和构造注入两种方式都成功装配了 Person 实例。

Spring基于Annotation装配Bean

在 Spring中,尽管使用 XML 配置文件可以实现 Bean 的装配工作,但如果应用中 Bean 的数量较多,会导致 XML 配置文件过于臃肿,从而给维护和升级带来一定的困难。

Java 从 JDK 5.0 以后,提供了 Annotation(注解)功能,Spring 也提供了对 Annotation 技术的全面支持。Spring3 中定义了一系列的 Annotation(注解),常用的注解如下。

1)@Component

可以使用此注解描述 Spring 中的 Bean,但它是一个泛化的概念,仅仅表示一个组件(Bean),并且可以作用在任何层次。使用时只需将该注解标注在相应类上即可。

2)@Repository

用于将数据访问层(DAO层)的类标识为 Spring 中的 Bean,其功能与 @Component 相同。

3)@Service

通常作用在业务层(Service 层),用于将业务层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。

4)@Controller

通常作用在控制层(如 Struts2的 Action),用于将控制层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。

5)@Autowired

用于对 Bean 的属性变量、属性的 Set 方法及构造函数进行标注,配合对应的注解处理器完成 Bean 的自动配置工作。默认按照 Bean 的类型进行装配。

6)@Resource

其作用与 Autowired 一样。其区别在于 @Autowired 默认按照 Bean 类型装配,而 @Resource 默认按照 Bean 实例名称进行装配。

@Resource 中有两个重要属性:name 和 type。

Spring 将 name 属性解析为 Bean 实例名称,type 属性解析为 Bean 实例类型。如果指定 name 属性,则按实例名称进行装配;如果指定 type 属性,则按 Bean 类型进行装配。

如果都不指定,则先按 Bean 实例名称装配,如果不能匹配,则再按照 Bean 类型进行装配;如果都无法匹配,则抛出 NoSuchBeanDefinitionException 异常。

7)@Qualifier

与 @Autowired 注解配合使用,会将默认的按 Bean 类型装配修改为按 Bean 的实例名称装配,Bean 的实例名称由 @Qualifier 注解的参数指定。

1. 创建 DAO 层接口

在 src 目录下创建一个名为 com.mengma.annotation 的包,在该包下创建一个名为 PersonDao 的接口,并添加一个 add() 方法,如下所示。

package com.mengma.annotation;
public interface PersonDao {
    public void add();
}
2. 创建 DAO 层接口的实现类

在 com.mengma.annotation 包下创建 PersonDao 接口的实现类 PersonDaoImpl,编辑后如下所示。

package com.mengma.annotation;
import org.springframework.stereotype.Repository;
@Repository("personDao")
public class PersonDaoImpl implements PersonDao {
    @Override
    public void add() {
        System.out.println("Dao层的add()方法执行了...");
    }
}

上述代码中,首先使用 @Repository 注解将 PersonDaoImpl 类标识为 Spring 中的 Bean,其写法相当于配置文件中<bean id="personDao"class="com.mengma.annotation.PersonDaoImpl"/>的书写。 然后在 add() 方法中输出一句话,用于验证是否成功调用了该方法。

3. 创建 Service 层接口

在 com.mengma.annotation 包下创建一个名为 PersonService 的接口,并添加一个 add() 方法,如下所示。

package com.mengma.annotation;
public interface PersonService {
    public void add();
}
4. 创建 Service 层接口的实现类

在 com.mengma.annotation 包下创建 PersonService 接口的实现类 PersonServiceImpl,编辑后如下所示。

package com.mengma.annotation;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
@Service("personService")
public class PersonServiceImpl implements PersonService {
    @Resource(name = "personDao")
    private PersonDao personDao;
    public PersonDao getPersonDao() {
        return personDao;
    }
    @Override
    public void add() {
        personDao.add();// 调用personDao中的add()方法
        System.out.println("Service层的add()方法执行了...");
    }
}

上述代码中,首先使用 @Service 注解将 PersonServiceImpl 类标识为 Spring 中的 Bean,其写法相当于配置文件中 <bean id="personService"class="com.mengma.annotation.PersonServiceImpl"/> 的书写。

然后使用 @Resource 注解标注在属性 personDao 上(也可标注在 personDao 的 setPersonDao() 方法上),这相当于配置文件中<property name="personDao"ref="personDao"/> 的写法。 最后在该类的 add() 方法中调用 personDao 中的 add() 方法,并输出一句话。

5. 创建 Action

在 com.mengma.annotation 包下创建一个名为 PersonAction 的类,编辑后如下所示。

package com.mengma.annotation;
import javax.annotation.Resource;
import org.springframework.stereotype.Controller;
@Controller("personAction")
public class PersonAction {
    @Resource(name = "personService")
    private PersonService personService;
    public PersonService getPersonService() {
        return personService;
    }
    public void add() {
        personService.add(); // 调用personService中的add()方法
        System.out.println("Action层的add()方法执行了...");
    }
}

上述代码中,首先使用 @Controller 注解标注 PersonAction 类,其写法相当于在配置文件中编写 <bean id="personAction"class="com.mengma.annotation.PersonAction"/>

然后使用了 @Resource 注解标注在 personService 上,这相当于在配置文件内编写 <property name="personService"ref="personService"/>

最后在其 add() 方法中调用了 personService 中的 add() 方法,并输出一句话。

6. 创建 Spring 配置文件

在 com.mengma.annotation 包下创建一个名为 applicationContext.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"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd">
    <!--使用context命名空间,通知spring扫描指定目录,进行注解的解析-->
    <context:component-scan base-package="com.mengma.annotation"/>
</beans>

与之前的配置文件相比,上述代码的< beans>元素中增加了第 7 行、第 15 行和第 16 行中包含有 context 的代码,然后在第 18 行代码中,使用 context 命名空间的 component-scan 元素进行注解的扫描,其 base-package 属性用于通知 spring 所需要扫描的目录。

7. 创建测试类

在 com.mengma.annotation 包下创建一个名为 AnnotationTest 的测试类,编辑后如下所示。

package com.mengma.annotation;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class AnnotationTest {
    @Test
    public void test() {
        // 定义Spring配置文件路径
        String xmlPath = "com/mengma/annotation/applicationContext.xml";
        // 初始化Spring容器,加载配置文件,并对bean进行实例化
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
                xmlPath);
        // 获得personAction实例
        PersonAction personAction = (PersonAction) applicationContext
                .getBean("personAction");
        // 调用personAction中的add()方法
        personAction.add();
    }
}

上述代码中,首先通过加载配置文件并获取 personAction 的实例,然后调用该实例的 add() 方法。

8. 运行程序并查看结果

使用 JUnit 测试运行 test() 方法,运行成功后,输出结果如图所示。

输出结果

从图的输出结果中可以看出,DAO 层、Service 层和 Action 层的 add() 方法都成功输出了结果。由此可知,使用 Annotation 装配 Bean 的方式已经成功实现了。

十一、Spring AOP(面向切面编程)是什么?

面向切面编程(AOP)和面向对象编程(OOP)类似,也是一种编程模式。SpringAOP 是基于 AOP 编程模式的一个框架,它的使用有效减少了系统间的重复代码,达到了模块间的松耦合目的。

AOP 的全称是“Aspect Oriented Programming”,即面向切面编程,它将业务逻辑的各个部分进行隔离,使开发人员在编写业务逻辑时可以专心于核心业务,从而提高了开发效率。

AOP 采取横向抽取机制,取代了传统纵向继承体系的重复性代码,其应用主要体现在事务处理、日志管理、权限控制、异常处理等方面。

目前最流行的 AOP 框架有两个,分别为 Spring AOP 和 AspectJ。

Spring AOP 使用纯 Java 实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类植入增强的代码。

AspectJ 是一个基于 Java 语言的 AOP 框架,从 Spring 2.0 开始,Spring AOP 引入了对 AspectJ 的支持。AspectJ 扩展了 Java 语言,提供了一个专门的编译器,在编译时提供横向代码的植入。

为了更好地理解 AOP,就需要对 AOP 的相关术语有一些了解,这些专业术语主要包含 Joinpoint、Pointcut、Advice、Target、Weaving、Proxy 和 Aspect,它们的含义如下表所示。

名称说明
Joinpoint(连接点)指那些被拦截到的点,在 Spring 中,可以被动态代理拦截目标类的方法。
Pointcut(切入点)指要对哪些 Joinpoint 进行拦截,即被拦截的连接点。
Advice(通知)指拦截到 Joinpoint 之后要做的事情,即对切入点增强的内容。
Target(目标)指代理的目标对象。
Weaving(植入)指把增强代码应用到目标上,生成代理对象的过程。
Proxy(代理)指生成的代理对象。
Aspect(切面)切入点和通知的结合。

img

img

十二、Spring JDK动态代理(附带实例)

JDK 动态代理是通过 JDK 中的 java.lang.reflect.Proxy 类实现的。下面通过具体的案例演示 JDK 动态代理的使用。

1. 创建项目

在 Eclipse 中创建一个名称为 springDemo03 的 Web 项目,将 Spring支持和依赖的 JAR 包复制到 Web 项目的 WEB-INF/lib 目录中,并发布到类路径下。

2. 创建接口 CustomerDao

在项目的 src 目录下创建一个名为 com.mengma.dao 的包,在该包下创建一个 CustomerDao 接口,编辑后如下所示。

package com.mengma.dao;
public interface CustomerDao {
    public void add(); // 添加
    public void update(); // 修改
    public void delete(); // 删除
    public void find(); // 查询
}
3. 创建实现类 CustomerDaoImpl

在 com.mengma.dao 包下创建 CustomerDao 接口的实现类 CustomerDaoImpl,并实现该接口中的所有方法,如下所示。

package com.mengma.dao;
public class CustomerDaoImpl implements CustomerDao {
    @Override
    public void add() {
        System.out.println("添加客户...");
    }
    @Override
    public void update() {
        System.out.println("修改客户...");
    }
    @Override
    public void delete() {
        System.out.println("删除客户...");
    }
    @Override
    public void find() {
        System.out.println("修改客户...");
    }
}
4. 创建切面类 MyAspect

在 src 目录下,创建一个名为 com.mengma.jdk 的包,在该包下创建一个切面类 MyAspect,编辑后如下所示。

package com.mengma.jdk;
public class MyAspect {
    public void myBefore() {
        System.out.println("方法执行之前");
    }
    public void myAfter() {
        System.out.println("方法执行之后");
    }
}

上述代码中,在切面中定义了两个增强的方法,分别为 myBefore() 方法和 myAfter() 方法,用于对目标类(CustomerDaoImpl)进行增强。

5. 创建代理类 MyBeanFactory

在 com.mengma.jdk 包下创建一个名为 MyBeanFactory 的类,在该类中使用 java.lang.reflect.Proxy 实现 JDK 动态代理,如下所示。

package com.mengma.jdk;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import com.mengma.dao.CustomerDao;
import com.mengma.dao.CustomerDaoImpl;
public class MyBeanFactory {
    public static CustomerDao getBean() {
        // 准备目标类
        final CustomerDao customerDao = new CustomerDaoImpl();
        // 创建切面类实例
        final MyAspect myAspect = new MyAspect();
        // 使用代理类,进行增强
        return (CustomerDao) Proxy.newProxyInstance(
                MyBeanFactory.class.getClassLoader(),
                new Class[] { CustomerDao.class }, new InvocationHandler() {
                    public Object invoke(Object proxy, Method method,
                            Object[] args) throws Throwable {
                        myAspect.myBefore(); // 前增强
                        Object obj = method.invoke(customerDao, args);
                        myAspect.myAfter(); // 后增强
                        return obj;
                    }
                });
    }
}

上述代码中,定义了一个静态的 getBean() 方法,这里模拟 Spring 框架的 IoC 思想,通过调用 getBean() 方法创建实例,第 14 行代码创建了 customerDao 实例。

第 16 行代码创建的切面类实例用于调用切面类中相应的方法;第 18~26 行就是使用代理类对创建的实例 customerDao 中的方法进行增强的代码,其中 Proxy 的 newProxyInstance() 方法的第一个参数是当前类的类加载器,第二参数是所创建实例的实现类的接口,第三个参数就是需要增强的方法。

在目标类方法执行的前后,分别执行切面类中的 myBefore() 方法和 myAfter() 方法。

6. 创建测试类 JDKProxyTest

在 com.mengma.jdk 包下创建一个名为 JDKProxyTest 的测试类,如下所示。

package com.mengma.jdk;
import org.junit.Test;
import com.mengma.dao.CustomerDao;
public class JDKProxyTest {
    @Test
    public void test() {
        // 从工厂获得指定的内容(相当于spring获得,但此内容时代理对象)
        CustomerDao customerDao = MyBeanFactory.getBean();
        // 执行方法
        customerDao.add();
        customerDao.update();
        customerDao.delete();
        customerDao.find();
    }
}

上述代码中,在调用 getBean() 方法时,获取的是 CustomerDao 类的代理对象,然后调用了该对象中的方法。

7. 运行项目并查看结果

使用 JUnit 测试运行 test() 方法,运行成功后,控制台的输出结果如图所示。

从图的输出结果中可以看出,在调用目标类的方法前后,成功调用了增强的代码,由此说明,JDK 动态代理已经实现。

运行结果

十三、Spring CGLlB动态代理(附带实例)

通过《Spring JDK动态代理》教程的学习可以知道,JDK 动态代理使用起来非常简单,但是它也有一定的局限性,这是因为 JDK 动态代理必须要实现一个或多个接口,如果不希望实现接口,则可以使用 CGLIB 代理。

CGLIB(Code Generation Library)是一个高性能开源的代码生成包,它被许多 AOP 框架所使用,其底层是通过使用一个小而快的字节码处理框架 ASM(Java 字节码操控框架)转换字节码并生成新的类。因此 CGLIB 要依赖于 ASM 的包,解压 Spring的核心包 spring-core-3.2.2.RELEASE.jar,文件目录如图所示。

spring-core-3.2.2.RELEASE.jar文件

在图中可以看出,解压的核心包中包含 cglib 和 asm,也就是说 Spring3.2.13 版本的核心包已经集成了 CGLIB 所需要的包,所以在开发中不需要另外导入 ASM 的 JAR 包了。下面通过案例演示实现 CGLIB 的代理过程。

1. 创建目标类 GoodsDao

在 com.mengma.dao 包下创建目标类 GoodsDao,在类中定义增、删、改、查方法,并在每个方法编写输出语句,如下所示。

package com.mengma.dao;
public class GoodsDao {
    public void add() {
        System.out.println("添加商品...");
    }
    public void update() {
        System.out.println("修改商品...");
    }
    public void delete() {
        System.out.println("删除商品...");
    }
    public void find() {
        System.out.println("修改商品...");
    }
}
2. 创建代理类 MyBeanFactory

在 src 目录下创建一个名为 com.mengma.cglib 的包,该包下创建类 MyBeanFactory,如下所示。

package com.mengma.cglib;
import java.lang.reflect.Method;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import com.mengma.dao.GoodsDao;
import com.mengma.jdk.MyAspect;
public class MyBeanFactory {
    public static GoodsDao getBean() {
        // 准备目标类
        final GoodsDao goodsDao = new GoodsDao();
        // 创建切面类实例
        final MyAspect myAspect = new MyAspect();
        // 生成代理类,CGLIB在运行时,生成指定对象的子类,增强
        Enhancer enhancer = new Enhancer();
        // 确定需要增强的类
        enhancer.setSuperclass(goodsDao.getClass());
        // 添加回调函数
        enhancer.setCallback(new MethodInterceptor() {
            // intercept 相当于 jdk invoke,前三个参数与 jdk invoke—致
            @Override
            public Object intercept(Object proxy, Method method, Object[] args,
                    MethodProxy methodProxy) throws Throwable {
                myAspect.myBefore(); // 前增强
                Object obj = method.invoke(goodsDao, args); // 目标方法执行
                myAspect.myAfter(); // 后增强
                return obj;
            }
        });
        // 创建代理类
        GoodsDao goodsDaoProxy = (GoodsDao) enhancer.create();
        return goodsDaoProxy;
    }
}

上述代码中,应用了 CGLIB 的核心类 Enhancer。在第 19 行代码调用了 Enhancer 类的 setSuperclass() 方法,确定目标对象。

第 21 行代码调用 setCallback() 方法添加回调函数;第 24 行代码的 intercept() 方法相当于 JDK 动态代理方式中的 invoke() 方法,该方法会在目标方法执行的前后,对切面类中的方法进行增强;第 33~34 行代码调用 Enhancer 类的 create() 方法创建代理类,最后将代理类返回。

3. 创建测试类

在 com.mengma.cglib 包下创建测试类 CGLIBProxyTest,编辑后如下所示。

package com.mengma.cglib;
import org.junit.Test;
import com.mengma.dao.GoodsDao;
public class CGLIBProxyTest {
    @Test
    public void test() {
        // 从工厂获得指定的内容(相当于spring获得,但此内容时代理对象)
        GoodsDao goodsDao = MyBeanFactory.getBean();
        // 执行方法
        goodsDao.add();
        goodsDao.update();
        goodsDao.delete();
        goodsDao.find();
    }
}

上述代码中,调用 getBean() 方法时,依然获取的是 goodsDao 的代理对象,然后调用该对象的方法。使用 JUnit 测试运行 test() 方法,运行成功后,控制台的输出结果如图所示。

输出结果
从图的输出结果中可以看出,在调用目标类的方法前后,也成功调用了增强的代码,由此说明,使用 CGLIB 代理的方式同样实现了手动代理。

十四、Spring通知类型及使用ProxyFactoryBean创建AOP代理

在《Spring JDK动态代理》和《Spring CGLlB动态代理》中,讲解了 AOP 手动代理的两种方式,下面通过讲解 Spring 的通知介绍 Spring 是如何创建 AOP 代理的。

Spring 通知类型

通过前面的学习可以知道,通知(Advice)其实就是对目标切入点进行增强的内容,Spring AOP 为通知(Advice)提供了 org.aopalliance.aop.Advice 接口。

Spring 通知按照在目标类方法的连接点位置,可以分为以下五种类型,如表 1 所示。

名称说明
org.springframework.aop.MethodBeforeAdvice(前置通知)在方法之前自动执行的通知称为前置通知,可以应用于权限管理等功能。
org.springframework.aop.AfterReturningAdvice(后置通知)在方法之后自动执行的通知称为后置通知,可以应用于关闭流、上传文件、删除临时文件等功能。
org.aopalliance.intercept.MethodInterceptor(环绕通知)在方法前后自动执行的通知称为环绕通知,可以应用于日志、事务管理等功能。
org.springframework.aop.ThrowsAdvice(异常通知)在方法抛出异常时自动执行的通知称为异常通知,可以应用于处理异常记录日志等功能。
org.springframework.aop.IntroductionInterceptor(引介通知)在目标类中添加一些新的方法和属性,可以应用于修改旧版本程序(增强类)。
声明式 Spring AOP

Spring 创建一个 AOP 代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean,这个类对应的切入点和通知提供了完整的控制能力,并可以生成指定的内容。

ProxyFactoryBean 类中的常用可配置属性如表 2 所示。

属性名称描 述
target代理的目标对象
proxyInterfaces代理要实现的接口,如果有多个接口,则可以使用以下格式赋值: …
proxyTargetClass是否对类代理而不是接口,设置为 true 时,使用 CGLIB 代理
interceptorNames需要植入目标的 Advice
singleton返回的代理是否为单例,默认为 true(返回单实例)
optimize当设置为 true 时,强制使用 CGLIB

在 Spring 通知中,环绕通知是一个非常典型的应用。下面通过环绕通知的案例演示 Spring 创建 AOP 代理的过程。

1. 导入 JAR 包

在核心 JAR 包的基础上,再向 springDemo03 项目的 lib 目录中导入 AOP 的 JAR 包,具体如下。

  • spring-aop-3.2.13.RELEASE.jar:是 Spring 为 AOP 提供的实现,在 Spring 的包中已经提供。
  • com.springsource.org.aopalliance-1.0.0.jar:是 AOP 提供的规范,可以在 Spring 的官网网址 https://repo.spring.io/webapp/#/search/quick/ 中进行搜索并下载。
2. 创建切面类 MyAspect

在 src 目录下创建一个名为 com.mengma.factorybean 的包,在该包下创建切面类 MyAspect,如下所示。

package com.mengma.factorybean;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
//需要实现接口,确定哪个通知,及告诉Spring应该执行哪个方法
public class MyAspect implements MethodInterceptor {
    public Object invoke(MethodInvocation mi) throws Throwable {
        System.out.println("方法执行之前");
        // 执行目标方法
        Object obj = mi.proceed();
        System.out.println("方法执行之后");
        return obj;
    }
}

上述代码中,MyAspect 类实现了 MethodInterceptor 接口,并实现了接口的 invoke() 方法。MethodInterceptor 接口是 Spring AOP 的 JAR 包提供的,而 invoke() 方法用于确定目标方法 mi,并告诉 Spring 要在目标方法前后执行哪些方法,这里为了演示效果在目标方法前后分别向控制台输出了相应语句。

3. 创建 Spring 配置文件

在 com.mengma.factorybean 包下创建配置文件 applicationContext.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="customerDao" class="com.mengma.dao.CustomerDaoImpl" />
    <!-- 通知 advice -->
    <bean id="myAspect" class="com.mengma.factorybean.MyAspect" />
    <!--生成代理对象 -->
    <bean id="customerDaoProxy"
     class="org.springframework.aop.framework.ProxyFactoryBean">
     <!--代理实现的接口 -->
        <property name="proxyInterfaces" value="com.mengma.dao.CustomerDao" />
        <!--代理的目标对象 -->
        <property name="target" ref="customerDao" />
        <!--用通知增强目标 -->
        <property name="interceptorNames" value="myAspect" />
        <!-- 如何生成代理,true:使用cglib; false :使用jdk动态代理 -->
        <property name="proxyTargetClass" value="true" />
    </bean>
</beans>

上述代码中,首先配置目标类和通知,然后使用 ProxyFactoryBean 类生成代理对象;第 14 行代码配置了代理实现的接口;第 16 行代码配置了代理的目标对象;第 18 行代码配置了需要植入目标的通知;当第 20 行代码中的 value 属性值为 true 时,表示使用 CGLIB 代理,属性值为 false 时,表示使用 JDK 动态代理。

4. 创建测试类

在 com.mengma.factorybean 包下创建一个名为 FactoryBeanTest 的测试类,编辑后如下所示。

package com.mengma.factorybean;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.mengma.dao.CustomerDao;
public class FactoryBeanTest {
    @Test
    public void test() {
        String xmlPath = "com/mengma/factorybean/applicationContext.xml";
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
                xmlPath);
        CustomerDao customerDao = (CustomerDao) applicationContext
                .getBean("customerDaoProxy");
        customerDao.add();
        customerDao.update();
        customerDao.delete();
        customerDao.find();
    }
}
5. 运行项目并查看结果

使用 JUnit 测试运行 test() 方法,运行成功后,控制台的输出结果如图所示。

运行结果

十五、Spring使用AspectJ开发AOP:基于XML/Annotation

AspectJ 是一个基于 Java语言的 AOP 框架,它扩展了 Java 语言。Spring 2.0 以后,新增了对 AspectJ 方式的支持,新版本的 Spring 框架,建议使用 AspectJ 方式开发 AOP。

使用 AspectJ 开发 AOP 通常有两种方式:

  • 基于 XML 的声明式。
  • 基于 Annotation 的声明式。

接下来将对这两种 AOP 的开发方式进行讲解。

基于XML的声明式

基于 XML 的声明式是指通过 Spring 配置文件的方式定义切面、切入点及声明通知,而所有的切面和通知都必须定义在 aop:config 元素中。

下面通过案例演示 Spring 中如何使用基于 XML 的声明式实现 AOP 的开发。

1. 导入 JAR 包

使用 AspectJ 除了需要导入 Spring AOP 的 JAR 包以外,还需要导入与 AspectJ 相关的 JAR 包,具体如下。

  • spring-aspects-3.2.13.RELEASE.jar:Spring 为 AspectJ 提供的实现,在 Spring 的包中已经提供。
  • com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar:是 AspectJ 提供的规范,可以在官方网址 https://repo.spring.io/webapp/#/search/quick/ 中搜索并下载。
2. 创建切面类 MyAspect

在 src 目录下创建一个名为 com.mengma.aspectj.xml 的包,在该包下创建切面类 MyAspect,编辑后如下所示。

package com.mengma.aspectj.xml;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
//切面类
public class MyAspect {
    // 前置通知
    public void myBefore(JoinPoint joinPoint) {
        System.out.print("前置通知,目标:");
        System.out.print(joinPoint.getTarget() + "方法名称:");
        System.out.println(joinPoint.getSignature().getName());
    }
    // 后置通知
    public void myAfterReturning(JoinPoint joinPoint) {
        System.out.print("后置通知,方法名称:" + joinPoint.getSignature().getName());
    }
    // 环绕通知
    public Object myAround(ProceedingJoinPoint proceedingJoinPoint)
            throws Throwable {
        System.out.println("环绕开始"); // 开始
        Object obj = proceedingJoinPoint.proceed(); // 执行当前目标方法
        System.out.println("环绕结束"); // 结束
        return obj;
    }
    // 异常通知
    public void myAfterThrowing(JoinPoint joinPoint, Throwable e) {
        System.out.println("异常通知" + "出错了" + e.getMessage());
    }
    // 最终通知
    public void myAfter() {
        System.out.println("最终通知");
    }
}

上述代码中,分别定义了几种不同的通知类型方法,在这些方法中,通过 JoinPoint 参数可以获得目标对象的类名、目标方法名和目标方法参数等。需要注意的是,环绕通知必须接收一个类型为 ProceedingJoinPoint 的参数,返回值必须是 Object 类型,且必须抛出异常。异常通知中可以传入 Throwable 类型的参数,用于输出异常信息。

3. 创建 Spring 配置文件

在 com.mengma.aspectj.xml 包下创建 applicationContext.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/beans
            http://www.springframework.org/schema/beans/spring-beans-2.5.xsd  
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
    <!--目标类 -->
    <bean id="customerDao" class="com.mengma.dao.CustomerDaoImpl" />
    <!--切面类 -->
    <bean id="myAspect" class="com.mengma.aspectj.xml.MyAspect"></bean>
    <!--AOP 编程 -->
    <aop:config>
        <aop:aspect ref="myAspect">
            <!-- 配置切入点,通知最后增强哪些方法 -->
            <aop:pointcut expression="execution ( * com.mengma.dao.*.* (..))"
                id="myPointCut" />
            <!--前置通知,关联通知 Advice和切入点PointCut -->
            <aop:before method="myBefore" pointeut-ref="myPointCut" />
            <!--后置通知,在方法返回之后执行,就可以获得返回值returning 属性 -->
            <aop:after-returning method="myAfterReturning"
                pointcut-ref="myPointCut" returning="returnVal" />
            <!--环绕通知 -->
            <aop:around method="myAround" pointcut-ref="myPointCut" />
            <!--抛出通知:用于处理程序发生异常,可以接收当前方法产生的异常 -->
            <!-- *注意:如果程序没有异常,则不会执行增强 -->
            <!-- * throwing属性:用于设置通知第二个参数的名称,类型Throwable -->
            <aop:after-throwing method="myAfterThrowing"
                pointcut-ref="myPointCut" throwing="e" />
            <!--最终通知:无论程序发生任何事情,都将执行 -->
            <aop:after method="myAfter" pointcut-ref="myPointCut" />
        </aop:aspect>
    </aop:config>
</beans>

上述代码中,首先在第 4、7、8 行代码中分别导入了 AOP 的命名空间。第 12 行代码指定了切面类。

第 17、18 行代码配置了切入点,通知需要增强哪些方法,expression="execution(com.mengma.dao..*(…))的意思是增强 com.mengma.dao 包下所有的方法。

第 20~32 行代码用于关联通知(Advice)和切入点(PointCut)。以第 20 行代码前置通知为例,aop:before 标签的 method 属性用于指定通知,pointcut-ref 属性用于指定切入点,也就是要增强的方法,其他几种通知的配置可以参考代码注释。

4. 创建测试类

在 com.mengma.aspectj.xml 包下创建测试类 XMLTest,如下所示。

package com.mengma.aspectj.xml;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.mengma.dao.CustomerDao;
public class XMLTest {
    @Test
    public void test() {
        String xmlPath = "com/mengma/aspectj/xml/applicationContext.xml";
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
                xmlPath);
        // 从spring容器获取实例
        CustomerDao customerDao = (CustomerDao) applicationContext
                .getBean("customerDao");
        // 执行方法
        customerDao.add();
    }
}
5. 运行项目并查看结果

使用 JUnit 测试运行 test() 方法,运行成功后,控制台的输出结果如图所示。

运行结果

为了更好地演示异常通知,接下来在 CustomerDaoImpl 类的 add() 方法中添加一行会抛出异常的代码,如“int i=1/0;”,重新运行 XMLTest 测试类,可以看到异常通知执行了,此时控制台的输出结果如图所示。

运行结果

从上面两张图的输出结果中可以看出,基于 XML 声明式的 AOP 开发已经成功实现。

基于 Annotation 的声明式

在 Spring 中,尽管使用 XML 配置文件可以实现 AOP 开发,但是如果所有的相关的配置都集中在配置文件中,势必会导致 XML 配置文件过于臃肿,从而给维护和升级带来一定的困难。

为此,AspectJ 框架为 AOP 开发提供了另一种开发方式——基于 Annotation 的声明式。AspectJ 允许使用注解定义切面、切入点和增强处理,而 Spring 框架则可以识别并根据这些注解生成 AOP 代理。

关于 Annotation 注解的介绍如表 1 所示。

名称说明
@Aspect用于定义一个切面。
@Before用于定义前置通知,相当于 BeforeAdvice。
@AfterReturning用于定义后置通知,相当于 AfterReturningAdvice。
@Around用于定义环绕通知,相当于MethodInterceptor。
@AfterThrowing用于定义抛出通知,相当于ThrowAdvice。
@After用于定义最终final通知,不管是否异常,该通知都会执行。
@DeclareParents用于定义引介通知,相当于IntroductionInterceptor(不要求掌握)。

下面使用注解的方式重新实现《基于XML的声明式》部分的功能。

1. 创建切面类 MyAspect

在 src 目录下创建一个名为 com.mengma.aspectj.annotation 的包,在该包下创建一个切面类 MyAspect,如下所示。

package com.mengma.aspectj.annotation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
//切面类
@Aspect
@Component
public class MyAspect {
    // 用于取代:<aop:pointcut
    // expression="execution(*com.mengma.dao..*.*(..))" id="myPointCut"/>
    // 要求:方法必须是private,没有值,名称自定义,没有参数
    @Pointcut("execution(*com.mengma.dao..*.*(..))")
    private void myPointCut() {
    }
    // 前置通知
    @Before("myPointCut()")
    public void myBefore(JoinPoint joinPoint) {
        System.out.print("前置通知,目标:");
        System.out.print(joinPoint.getTarget() + "方法名称:");
        System.out.println(joinPoint.getSignature().getName());
    }
    // 后置通知
    @AfterReturning(value = "myPointCut()")
    public void myAfterReturning(JoinPoint joinPoint) {
        System.out.print("后置通知,方法名称:" + joinPoint.getSignature().getName());
    }
    // 环绕通知
    @Around("myPointCut()")
    public Object myAround(ProceedingJoinPoint proceedingJoinPoint)
            throws Throwable {
        System.out.println("环绕开始"); // 开始
        Object obj = proceedingJoinPoint.proceed(); // 执行当前目标方法
        System.out.println("环绕结束"); // 结束
        return obj;
    }
    // 异常通知
    @AfterThrowing(value = "myPointCut()", throwing = "e")
    public void myAfterThrowing(JoinPoint joinPoint, Throwable e) {
        System.out.println("异常通知" + "出错了" + e.getMessage());
    }
    // 最终通知
    @After("myPointCut()")
    public void myAfter() {
        System.out.println("最终通知");
    }
}

上述代码中,第 13 行 @Aspect 注解用于声明这是一个切面类,该类作为组件使用,所以要添加 @Component 注解才能生效。第 19 行中 @Poincut 注解用于配置切入点,取代 XML 文件中配置切入点的代码。

在每个通知相应的方法上都添加了注解声明,并且将切入点方法名“myPointCut”作为参数传递给要执行的方法,如需其他参数(如异常通知的异常参数),可以根据代码提示传递相应的属性值。

2. 为目标类添加注解

在 com.mengma.dao.CustomerDaoImpl 目标类中添加注解 @Repository(“customerDao”)。

3. 创建Spring配置文件

在 com.mengma.aspectj.annotation 包下创建 applicationContext.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"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd">
    <!--扫描含com.mengma包下的所有注解-->
    <context:component-scan base-package="com.mengma"/>
    <!-- 使切面开启自动代理 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

上述代码中,首先导入了 AOP 命名空间及其配套的约束,使切面类中的 @AspectJ 注解能够正常工作;第 13 行代码添加了扫描包,使注解生效。需要注意的是,这里还包括目标类 com.mengma.dao.CustomerDaoImpl 的注解,所以 base-package 的值为 com.mengma;第 15 行代码的作用是切面开启自动代理。

4. 创建测试类

在 com.mengma.aspectj.annotation 包下创建一个名为 AnnotationTest 的测试类,如下所示。

package com.mengma.aspectj.annotation;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.mengma.dao.CustomerDao;
public class AnnotationTest {
    @Test
    public void test() {
        String xmlPath = "com/mengma/aspectj/xml/applicationContext.xml";
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
                xmlPath);
        // 从spring容器获取实例
        CustomerDao customerDao = (CustomerDao) applicationContext
                .getBean("customerDao");
        // 执行方法
        customerDao.add();
    }
}
5. 运行项目并查看结果

使用 JUnit 测试运行 test() 方法,运行成功后,控制台的输出结果如图所示。

运行结果

删除 add() 方法中的“int i=1/0;”,重新运行 test() 方法,此时控制台的输出结果如图所示。

运行结果

从上面两张图的输出结果中可以看出,已成功使用 Annotation 的方式实现了 AOP 开发。与其他方式相比,基于 Annotation 方式实现 AOP 的效果是最方便的方式,所以实际开发中推荐使用注解的方式。

十六、Spring事务管理的三个核心接口

Spring的事务管理是基于 AOP 实现的,而 AOP 是以方法为单位的。Spring 的事务属性分别为传播行为、隔离级别、只读和超时属性,这些属性提供了事务应用的方法和描述策略。

在 JavaEE 开发经常采用的分层模式中,Spring 的事务处理位于业务逻辑层,它提供了针对事务的解决方案。

在 Spring 解压包的 libs 目录中,包含一个名称为 spring-tx-3.2.13.RELEASE.jar 的文件,该文件是 Spring 提供的用于事务管理的 JAR 包,其中包括事务管理的三个核心接口:PlatformTransactionManager、TransactionDefinition 和 TransactionStatus。

将该 JAR 包的后缀名 jar 改成 zip 的形式后,解压压缩包,进入解压文件夹中的 \org\springframework\transaction 目录后,该目录中的文件如图所示。

事务管理核心接口

在图中,方框所标注的三个文件就是本节将要讲解的核心接口。这三个核心接口的作用及其提供的方法如下。

1. PlatformTransactionManager

PlatformTransactionManager 接口是 Spring 提供的平台事务管理器,用于管理事务。该接口中提供了三个事务操作方法,具体如下。

  • TransactionStatus getTransaction(TransactionDefinition definition):用于获取事务状态信息。
  • void commit(TransactionStatus status):用于提交事务。
  • void rollback(TransactionStatus status):用于回滚事务。

在项目中,Spring 将 xml 中配置的事务详细信息封装到对象 TransactionDefinition 中,然后通过事务管理器的 getTransaction() 方法获得事务的状态(TransactionStatus),并对事务进行下一步的操作。

2. TransactionDefinition

TransactionDefinition 接口是事务定义(描述)的对象,它提供了事务相关信息获取的方法,其中包括五个操作,具体如下。

  • String getName():获取事务对象名称。
  • int getIsolationLevel():获取事务的隔离级别。
  • int getPropagationBehavior():获取事务的传播行为。
  • int getTimeout():获取事务的超时时间。
  • boolean isReadOnly():获取事务是否只读。

在上述五个方法的描述中,事务的传播行为是指在同一个方法中,不同操作前后所使用的事务。传播行为的种类如表 1 所示。

属性名称描 述
PROPAGATION_REQUIREDrequired支持当前事务。如果 A 方法已经在事务中,则 B 事务将直接使用。否则将创建新事务
PROPAGATION_SUPPORTSsupports支持当前事务。如果 A 方法已经在事务中,则 B 事务将直接使用。否则将以非事务状态执行
PROPAGATION_MANDATORYmandatory支持当前事务。如果 A 方法没有事务,则抛出异常
PROPAGATION_REQUIRES_NEWrequires_new将创建新的事务,如果 A 方法已经在事务中,则将 A 事务挂起
PROPAGATION_NOT_SUPPORTEDnot_supported不支持当前事务,总是以非事务状态执行。如果 A 方法已经在事务中,则将其挂起
PROPAGATION_NEVERnever不支持当前事务,如果 A 方法在事务中,则抛出异常
PROPAGATION.NESTEDnested嵌套事务,底层将使用 Savepoint 形成嵌套事务

在事务管理过程中,传播行为可以控制是否需要创建事务以及如何创建事务。

通常情况下,数据的查询不会改变原数据,所以不需要进行事务管理,而对于数据的增加、修改和删除等操作,必须进行事务管理。如果没有指定事务的传播行为,则 Spring3 默认的传播行为是 required

3. TransactionStatus

TransactionStatus 接口是事务的状态,它描述了某一时间点上事务的状态信息。其中包含六个操作,具体如表 2 所示。

名称说明
void flush()刷新事务
boolean hasSavepoint()获取是否存在保存点
boolean isCompleted()获取事务是否完成
boolean isNewTransaction()获取是否是新事务
boolean isRollbackOnly()获取是否回滚
void setRollbackOnly()设置事务回滚

十七、Spring实现事务管理:基于XML/Annotation方式实现

基于XML方式实现

Spring的事务管理有两种方式:一种是传统的编程式事务管理,即通过编写代码实现的事务管理;另一种是基于 AOP 技术实现的声明式事务管理。由于在实际开发中,编程式事务管理很少使用,所以我们只对 Spring 的声明式事务管理进行详细讲解。

Spring 声明式事务管理在底层采用了 AOP 技术,其最大的优点在于无须通过编程的方式管理事务,只需要在配置文件中进行相关的规则声明,就可以将事务规则应用到业务逻辑中。

Spring 实现声明式事务管理主要有两种方式:

  • 基于 XML 方式的声明式事务管理。
  • 通过 Annotation 注解方式的事务管理。

本节通过银行转账的案例讲解如何使用 XML 的方式实现 Spring 的声明式事务处理。

1. 创建项目

在 Eclipse 中创建一个名为 springDemo03 的 Web 项目,将 Spring 支持和依赖的 JAR 包复制到 Web 项目的 lib 目录中,并添加到类路径下。所添加的 JAR 包如图所示。

需要导入的JAR包
从图中可以看出,这里增加导入了 spring-tx-3.2.13.RELEASE.jar(事务管理),以及 MySQL驱动、JDBC 和 C3P0 的 JAR 包。

2. 创建数据库、表以及插入数据

在 MySQL 中创建一个名为 spring 的数据库,然后在该数据库中创建一个 account 表,并向表中插入两条数据,其 SQL 执行语句如下所示:

CREATE DATABASE spring;
USE spring;
CREATE TABLE account (
    id INT (11) PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(20) NOT NULL,
    money INT DEFAULT NULL
);
INSERT INTO account VALUES (1,'zhangsan',1000);
INSERT INTO account VALUES (2,'lisi',1000);

执行后的 account 表中的数据如图所示。

执行结果

3. 创建 c3p0-db.properties

在项目的 src 下创建一个名为 c3p0-db.properties 的配置文件,这里使用 C3P0 数据源,需要在该文件中添加如下配置:

jdbc.driverClass = com.mysql.jdbc.Driver
jdbc.jdbcUrl = jdbc:mysql://localhost:3306/spring
jdbc.user = root
jdbc.password = root
4. 实现 DAO

1)创建 AccountDao 接口

在项目的 src 目录下创建一个名为 com.mengma.dao 的包,在该包下创建一个接口 AccountDao,并在接口中创建汇款和收款的方法,如下所示。

package com.mengma.dao;
public interface AccountDao {
    // 汇款
    public void out(String outUser, int money);
    // 收款
    public void in(String inUser, int money);
}

上述代码中,定义了 out() 和 in() 两个方法,分别用于表示汇款和收款。

2)创建DAO层接口实现类

在项目的 src 目录下创建一个名为 com.mengma.dao.impl 的包,在该包下创建实现类 AccountDaoImpl,如下所示。

package com.mengma.dao.impl;
import org.springframework.jdbc.core.JdbcTemplate;
import com.mengma.dao.AccountDao;
public class AccountDaoImpl implements AccountDao {
    private JdbcTemplate jdbcTemplate;
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    // 汇款的实现方法
    public void out(String outUser, int money) {
        this.jdbcTemplate.update("update account set money =money-?"
                + "where username =?", money, outUser);
    }
    // 收款的实现方法
    public void in(String inUser, int money) {
        this.jdbcTemplate.update("update account set money =money+?"
                + "where username =?", money, inUser);
    }
}

上述代码中,使用 JdbcTemplate 类的 update() 方法实现了更新操作。

5. 实现 Service

1)创建 Service 层接口

在项目的 src 目录下创建一个名为 com.mengma.service 的包,在该包下创建接口 AccountService,如下所示。

package com.mengma.service;
public interface AccountService {
    // 转账
    public void transfer(String outUser, String inUser, int money);
}

2)创建 Service 层接口实现类

在项目的 src 目录下创建一个名为 com.mengma.service.impl 的包,在该包下创建实现类 AccountServiceImpl,如下所示。

package com.mengma.service.impl;
import com.mengma.dao.AccountDao;
public class AccountServiceImpl {
    private AccountDao accountDao;
    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }
    public void transfer(String outUser, String inUser, int money) {
        this.accountDao.out(outUser, money);
        this.accountDao.in(inUser, money);
    }
}

上述代码中可以看出,该类实现了 AccountService 接口,并对转账的方法进行了实现,根据参数的不同调用 DAO 层相应的方法。

6. 创建 Spring 配置文件

在项目的 src 目录下创建 Spirng 配置文件 applicationContext.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:context="http://www.springframework.org/schema/context"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-2.5.xsd  
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
    <!-- 加载properties文件 -->
    <context:property-placeholder location="classpath:c3p0-db.properties" />
    <!-- 配置数据源,读取properties文件信息 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${jdbc.driverClass}" />
        <property name="jdbcUrl" value="${jdbc.jdbcUrl}" />
        <property name="user" value="${jdbc.user}" />
        <property name="password" value="${jdbc.password}" />
    </bean>
    <!-- 配置jdbc模板 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource" />
    </bean>
    <!-- 配置dao -->
    <bean id="accountDao" class="com.mengma.dao.impl.AccountDaoImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate" />
    </bean>
    <!-- 配置service -->
    <bean id="accountService" class="com.mengma.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao" />
    </bean>
    <!-- 事务管理器,依赖于数据源 -->
    <bean id="txManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>
    <!-- 编写通知:对事务进行增强(通知),需要编写切入点和具体执行事务的细节 -->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <!-- 给切入点方法添加事务详情,name表示方法名称,*表示任意方法名称,propagation用于设置传播行为,read-only表示隔离级别,是否只读 -->
            <tx:method name="find*" propagation="SUPPORTS"
                rollback-for="Exception" />
            <tx:method name="*" propagation="REQUIRED" isolation="DEFAULT"
                read-only="false" />
        </tx:attributes>
    </tx:advice>
    <!-- aop编写,让Spring自动对目标生成代理,需要使用AspectJ的表达式 -->
    <aop:config>
        <!-- 切入点 -->
        <aop:pointcut expression="execution(* com.mengma.service.*.*(..))"
            id="txPointCut" />
        <!-- 切面:将切入点与通知整合 -->
        <aop:advisor pointcut-ref="txPointCut" advice-ref="txAdvice" />
    </aop:config>
</beans>

上述代码中,首先在 < beans> 标记的第 6、13 和 14 行代码分别添加了 AOP 所需的命名空间声明。第 42~50 行代码使用 tx:advice 标记配置事务通知内容。

第 52~58 行代码使用 aop:config 标记定义切面,其中第 54 行代码应用了 AspectJ 表达式,代表 com.mengma.service 包下所有类的所有方法都应用事务规则,第 57 行代码使用 aop:advistor 标记将切入点与事务通知整合,基于 AOP 的声明式事务配置完成。

7. 创建测试类

在项目的 src 目录下创建 com.mengma.test 的包,在该包下创建测试类 AccountTest,如下所示。

package com.mengma.test;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.mengma.service.AccountService;
public class AccountTest {
    @Test
    public void test() {
        // 获得Spring容器,并操作
        String xmlPath = "applicationContext.xml";
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
                xmlPath);
        AccountService accountService = (AccountService) applicationContext
                .getBean("accountService");
        accountService.transfer("zhangsan", "lisi", 100);
    }
}

上述代码中模拟了银行转账业务,从 zhangsan 的账户向 lisi 的账户中转入 100 元。使用 JUnit 测试运行 test() 方法,运行成功后,查询 account 表,如图 3 所示。

从图的查询结果中可以看出,zhangsan 成功向 lisi 转账 100 元。

查询结果

下面通过修改案例模拟转账失败的情况,在的 transfer() 方法中添加一行代码“int i=1/0;”模拟系统断电的情况,具体代码如下所示:

public void transfer(String outUser, String inUser, int money) {
    this.accountDao.out(outUser, money);
    //模拟断电
    int i = 1/0;
    this.accountDao.in(inUser, money);
}

重新测试运行 test() 方法,JUnit 控制台输出的信息如图所示。

控制台输出结果

从上图中可以看出,在执行测试方法时,出现了除以 0 的异常信息。此时再次查询 account 表,其查询结果如上图所示。

从下图的查询结果中可以看出,表中的数据并没有发生变化。由于程序在执行过程中抛出了异常,事务不能正常被提交,所以转账失败。由此可知,Spring 的事务管理生效了。

查询结果

基于Annotation方式实现

在 Spring中,除了使用基于 XML 的方式可以实现声明式事务管理以外,还可以通过 Annotation 注解的方式实现声明式事务管理。

使用 Annotation 的方式非常简单,只需要在项目中做两件事,具体如下。

1)在 Spring 容器中注册驱动,代码如下所示:

<tx:annotation-driven transaction-manager="txManager"/>

2)在需要使用事务的业务类或者方法中添加注解 @Transactional,并配置 @Transactional 的参数。关于 @Transactional 的参数如图所示。

@Transactional参数列表

下面通过修改《Spring基于XML实现事务管理》教程中银行转账的案例讲解如何使用 Annotation 注解的方式实现 Spring 声明式事务管理。

1. 注册驱动

修改 Spring 配置文件 applicationContext.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:context="http://www.springframework.org/schema/context"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-2.5.xsd  
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
    <!-- 加载properties文件 -->
    <context:property-placeholder location="classpath:c3p0-db.properties" />
    <!-- 配置数据源,读取properties文件信息 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${jdbc.driverClass}" />
        <property name="jdbcUrl" value="${jdbc.jdbcUrl}" />
        <property name="user" value="${jdbc.user}" />
        <property name="password" value="${jdbc.password}" />
    </bean>
    <!-- 配置jdbc模板 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource" />
    </bean>
    <!-- 配置dao -->
    <bean id="accountDao" class="com.mengma.dao.impl.AccountDaoImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate" />
    </bean>
    <!-- 配置service -->
    <bean id="accountService" class="com.mengma.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao" />
    </bean>
    <!-- 事务管理器,依赖于数据源 -->
    <bean id="txManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>
    <!-- 注册事务管理驱动 -->
    <tx:annotation-driven transaction-manager="txManager"/>
</beans>

上述代码中可以看出,与原来的配置文件相比,这里只修改了事务管理器部分,新添加并注册了事务管理器的驱动。

需要注意的是,在学习 AOP 注解方式开发时,需要在配置文件中开启注解处理器,指定扫描哪些包下的注解,这里没有开启注解处理器是因为在第 33~35 行手动配置了 AccountServiceImpl,而 @Transactional 注解就配置在该类中,所以会直接生效。

2. 添加 @Transactional 注解

修改 AccountServiceImpl,在文件中添加 @Transactional 注解及参数,添加后如下所示。

package com.mengma.service.impl;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.mengma.dao.AccountDao;
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, readOnly = false)
public class AccountServiceImpl {
    private AccountDao accountDao;
    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }
    public void transfer(String outUser, String inUser, int money) {
        this.accountDao.out(outUser, money);
        // 模拟断电
        int i = 1 / 0;
        this.accountDao.in(inUser, money);
    }
}

需要注意的是,在使用 @Transactional 注解时,参数之间用“,”进行分隔。

使用 JUnit 测试再次运行 test() 方法时,控制台同样会输出如图所示的异常信息,这说明使用基于 Annotation 注解的方式同样实现了 Spring 的声明式事务管理。如果注释掉模拟断电的代码进行测试,则转账操作可以正常完成。

运行结果

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐