Spring5-学习手册-全-

Spring5 学习手册(全)

原文:zh.annas-archive.org/md5/6DF1C981F26DA121DCB8C1B33E7DE022

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章。Spring 概览

Spring,传统 J2EE 的冬天之后的新起点,这就是 Spring 框架的真正含义。是处理 Java 企业应用程序中众多复杂模块相互协作开发的最问题解决方案。Spring 不是传统 Java 开发的替代品,而是公司应对当今竞争激烈、快速发展的市场的可靠解决方案,同时不让开发者紧密依赖于 Spring API。

在本主题中,我们将涉及以下几点:

  • Spring 框架简介

  • Spring 在企业应用程序开发中解决的问题

  • Spring 路线图

  • Spring 5.0 的新特性

spring 框架简介

引言

罗德·约翰逊是澳大利亚计算机专家,也是 SpringSource 的联合创始人。《J2EE 设计和发展一对一专家》于 2002 年 11 月由他出版。这本书包含大约 30000 行代码,其中包括框架的基本概念,如控制反转IoC)和依赖注入DI)。这部分代码被称为 interface21。他写这部分代码的初衷只是为了方便开发者工作,或者作为他们自己开发的基石。他从未想过开发任何框架或类似的东西。在 Wrox 论坛上,关于代码、其改进和其他许多事情进行了长时间的讨论。论坛上的两位读者尤尔根·霍勒和扬·卡罗夫提出了将代码作为新框架基础的想法。这是扬的理由,Spring,传统 J2EE 的冬天之后的新起点,他将框架命名为 Spring 框架。该项目于 2003 年 6 月公开,并朝着 1.0 版本迈进。此后,为了支持市场上的技术,发生了许多变化和升级。本书关注的是最新版本 5.0。在接下来的几页中,我们将介绍这个版本中添加的新特性。在随后的页面中,我们将介绍如何将最新特性应用于您的应用程序,以及作为开发人员如何充分利用这些特性。

Spring 解决的问题

引言

Java 平台是一个长期、复杂、可扩展、积极进取并快速发展的平台。应用程序开发在特定的版本上进行。为了保持最新的标准和与之相适应,这些应用程序需要不断升级到最新版本。这些应用程序有大量的类相互交互,复用 API 以充分发挥其优势,使应用程序运行顺畅。但这导致了 AS 的许多常见问题。

可扩展性

市场上的每项技术,无论是硬件还是软件,其增长和发展的速度都非常快。几年前开发的应用程序可能会因为这些领域的增长而变得过时。市场的要求如此之高,以至于开发者需要不断更新应用程序。这意味着我们今天开发的任何应用程序都应能在不影响现有运行的情况下,处理未来的需求和增长。应用程序的可扩展性是处理或支持工作负载的增加,以适应不断增长的环境,而不是替换它们。应用程序能够支持因用户数量增加而增加的网站流量,这是一个非常简单的例子,说明应用程序是可扩展的。由于代码紧密耦合,使其可扩展成为一个问题。

管道代码

让我们以在 Tomcat 环境中配置 DataSource 为例。现在开发者想要在应用程序中使用这个配置的 DataSource。我们会做什么?是的,我们会进行 JNDI 查找以获取 DataSource。为了处理 JDBC,我们将获取资源并在try catch中释放。像我们在这里讨论的try catch,计算机间的通信,集合等都是必要的,但不是特定于应用程序的,这些是管道代码。管道代码增加了代码的长度,并使调试变得复杂。

样板代码

我们在进行 JDBC 时如何获取连接?我们需要注册 Driver 类,并在 DriverManager 上调用getConnection()方法以获取连接对象。这些步骤有其他替代方案吗?实际上没有!无论何时,无论在哪里进行 JDBC,这些相同的步骤每次都必须重复。这种重复的代码,开发者在不同地方编写的代码块,少量或没有修改以实现某些任务,称为样板代码。样板代码使 Java 开发变得不必要地更长和更复杂。

无法避免的非功能性代码

无论何时进行应用程序开发,开发者都会专注于业务逻辑、外观和要实现的数据持久性。但除了这些事情,开发者还会深入思考如何管理事务,如何处理网站上的增加负载,如何使应用程序安全等问题。如果我们仔细观察,这些事情并不是应用程序的核心关注点,但它们却是无法避免的。这种不处理业务逻辑(功能性)需求,但对维护、故障排除、应用程序安全等重要的代码称为非功能性代码。在大多数 Java 应用程序中,开发者经常不得不编写非功能性代码。这导致对业务逻辑开发产生了偏见。

单元测试应用程序

让我们来看一个例子。我们希望测试一段将数据保存到数据库中的代码。这里测试数据库不是我们的目的,我们只是想确定我们编写的代码是否正常工作。企业级 Java 应用程序由许多相互依赖的类组成。由于对象之间存在依赖,因此进行测试变得困难。

Spring 主要解决了这些问题,并提供了一个非常强大而又简单的解决方案,

基于 POJO 的开发

类是应用程序开发的基本结构。如果类被扩展或实现了框架的接口,由于它们与 API 紧密耦合,因此复用变得困难。普通老式 Java 对象POJO)在 Java 应用程序开发中非常著名且经常使用。与 Struts 和 EJB 不同,Spring 不会强制开发者编写导入或扩展 Spring API 的代码。Spring 最好的地方在于,开发者可以编写通常不依赖于框架的代码,为此,POJO 是首选。POJO 支持松耦合的模块,这些模块可复用且易于测试。

注意

Spring 框架之所以被称为非侵入性,是因为它不会强制开发者使用 API 类或接口,并允许开发松耦合的应用程序。

通过依赖注入实现松耦合

耦合度是类与类之间知识的关联程度。当一个类对其他类的设计依赖性较低时,这个类就可以被称为松耦合。松耦合最好通过接口编程来实现。在 Spring 框架中,我们可以将类的依赖关系在与代码分离的配置文件中维护。利用 Spring 提供的接口和依赖注入技术,开发者可以编写松耦合的代码(别担心,我们很快就会讨论依赖注入以及如何实现它)。借助松耦合,开发者可以编写出因依赖变化而需要频繁变动的代码。这使得应用程序更加灵活和易于维护。

声明式编程

在声明式编程中,代码声明了将要执行什么,而不是如何执行。这与命令式编程完全相反,在命令式编程中,我们需要逐步说明我们将执行什么。声明式编程可以通过 XML 和注解来实现。Spring 框架将所有配置保存在 XML 中,框架可以从中维护 bean 的生命周期。由于 Spring 框架中的开发,从 2.0 版本开始提供了 XML 配置的替代方案,即使用广泛的注解。

使用方面和模板减少样板代码

我们刚刚在前几页讨论过,重复的代码是样板代码。样板代码是必要的,如果没有它,提供事务、安全、日志等功能将变得困难。框架提供了解决编写处理此类交叉关注点的 Aspect 的方案,无需将其与业务逻辑代码一起编写。使用 Aspect 有助于减少样板代码,但开发者仍然可以实现相同的效果。框架提供的另一个功能是不同需求的模板。JDBCTemplate、HibernateTemplate 是 Spring 提供的另一个有用的概念,它减少了样板代码。但事实上,你需要等待理解并发现其真正的潜力。

分层架构

与分别提供 Web 持久性解决方案的 Struts 和 Hibernate 不同,Spring 有一系列广泛的模块解决多种企业开发问题。这种分层架构帮助开发者选择一个或多个模块,以一种连贯的方式为他的应用程序编写解决方案。例如,即使不知道框架中有许多其他模块,开发者也可以选择 Web MVC 模块高效处理 Web 请求。

Spring 架构


Spring 提供了超过 20 个不同的模块,可以大致归纳为 7 个主要模块,如下所示:

Spring 模块

核心模块

核心

Spring 核心模块支持创建 Spring bean 的方法以及向 bean 中注入依赖。它提供了配置 bean 以及如何使用 BeanFactory 和 ApplicationContext 从 Spring 容器获取配置 bean 的方法,用于开发独立应用程序。

Beans

Beans 模块提供了BeanFactory,为编程单例提供了替代方案。BeanFactory 是工厂设计模式的实现。

上下文

这个模块支持诸如 EJB、JMX 和基本远程调用的 Java 企业特性。它支持缓存、Java 邮件和模板引擎(如 Velocity)的第三方库集成。

SpEL

Spring 表达式语言(SpEL)统一表达式语言的扩展,该语言已在 JSP 2.1 规范中指定。SpEL 模块支持设置和获取属性值,使用逻辑和算术运算符配置集合,以及从 Spring IoC 中获取命名变量。

数据访问与集成模块

JDBC(DAO)

这个模块在 JDBC 之上提供了抽象层。它支持减少通过加载驱动器获取连接对象、获取语句对象等产生的样板代码。它还支持模板,如 JdbcTemplate、HibernateTemplate,以简化开发。

ORM

对象关系映射(ORM)模块支持与非常流行的框架(如 Hibernate、iBATIS、Java 持久性 API(JPA)、Java 数据对象(JDO))的集成。

OXM

对象 XML 映射器(OXM)模块支持对象到 XML 的映射和集成,适用于 JAXB、Castor、XStream 等。

JMS

此模块提供支持,并为通过消息进行异步集成的 Java 消息服务(JMS)提供 Spring 抽象层。

事务

JDBC 和 ORM 模块处理 Java 应用程序与数据库之间的数据交换。此模块在处理 ORM 和 JDBC 模块时支持事务管理。

Web MVC 和远程模块

Web

此模块支持与其他框架创建的 web 应用程序的集成。使用此模块,开发人员还可以通过 Servlet 监听器开发 web 应用程序。它支持多部分文件上传和请求与响应的处理。它还提供了与 web 相关的远程支持。

Servlet

此模块包含 Spring 模型视图控制器(MVC)实现,用于 web 应用程序。使用 Spring MVC,开发人员可以编写处理请求和响应的代码,以开发功能齐全的 web 应用程序。它通过支持处理表单提交来摆脱处理请求和响应的样板代码。

门户

门户模块提供了 MVC 实现,用于支持 Java 门户 API 的门户环境。

注意

门户在 Spring 5.0M1 中被移除。如果您想使用门户,则需要使用 4.3 模块。

WebSocket

WebSocket 是一种协议,提供客户端与服务器之间的双向通信,已在 Spring 4 中包含。此模块为应用程序提供对 Java WebSocket API 的集成支持。

注意

Struts 模块 此模块包含支持将 Struts 框架集成到 Spring 应用程序中的内容。但在 Spring 3.0 中已弃用。

AOP 模块

AOP

面向方面编程(AOP)模块有助于处理和管理应用程序中的交叉关注点服务,有助于保持代码的清洁。

方面

此模块为 AspectJ 提供了集成支持。

仪器模块

仪器

Java 仪器提供了一种创新的方法,通过类加载器帮助从 JVM 访问类并修改其字节码,通过插入自定义代码。此模块支持某些应用服务器的仪器和类加载器实现。

仪器 Tomcat

仪器 Tomcat 模块包含对 Tomcat 的 Spring 仪器支持。

消息传递

消息传递模块提供了对 STOMP 作为 WebSocket 协议的支持。它还有用于路由和处理从客户端接收的 STOMP 消息的注解。

  • Spring 4 中包含了消息传递模块。

测试模块

测试模块支持单元以及集成测试,适用于 JUnit 和 TestNG。它还提供创建模拟对象的支持,以简化在隔离环境中进行的测试。

Spring 还支持哪些底层技术?


安全模块

如今,仅具有基本功能的应用程序也需要提供良好的多层次安全处理方式。Spring5 支持使用 Spring AOP 的声明式安全机制。

批量处理模块

Java 企业应用需要执行批量处理,在没有用户交互的情况下处理大量数据,这在许多商业解决方案中是必需的。以批处理方式处理这些问题是可用的最佳解决方案。Spring 提供了批量处理集成,以开发健壮的应用程序。

Spring Integration

在企业应用的开发中,应用可能需要与它们进行交互。Spring Integration 是 Spring 核心框架的扩展,通过声明式适配器提供与其他企业应用的集成。消息传递是此类集成中广泛支持的一项。

移动模块

广泛使用移动设备为开发打开了新的大门。这个模块是 Spring MVC 的扩展,有助于开发称为 Spring Android Project 的手机网络应用。它还提供检测发起请求的设备类型,并相应地呈现视图。

LDAP 模块

Spring 的初衷是简化开发并减少 boilerplate 代码。Spring LDAP 模块支持使用模板开发进行简单的 LDAP 集成。

.NEW 模块

引入了新的模块来支持.NET 平台。ADO.NET、NHibernate、ASP.NET 等模块包含在.NET 模块中,以简化.NET 开发,利用 DI、AOP、松耦合等特性。

Spring 路线图


1.0 2004 年 3 月

它支持 JDO1.0 和 iBATIS 1.3,并与 Spring 事务管理集成。这个版本支持的功能有,Spring Core、Spring Context、Spring AOP、Spring DAO、Spring ORM 和 Spring web。

2.0 2006 年 10 月

Spring 框架增强了 Java5 的支持。它添加了开箱即用的命名空间,如 jee、tx、aop、lang、util,以简化配置。IoC 支持单例和原型等作用域。除了这些作用域,还引入了 HttpSession、集群缓存和请求的作用域。引入了基于注解的配置,如@Transactional、@Required、@PersistenceContext。

2.5 2007 年 11 月

在这个版本中,Spring 支持完整的 Java 6 和 Java EE 5 特性,包括 JDBC 4、JavMail 1.4、JTA 1.1、JAX WS 2.0。它还扩展了对基于注解的依赖注入的支持,包括对限定符的支持。引入了一个名为 AspectJ 切点表达式中的 pointcut 元素的新 bean。提供了基于 LoadTimeWeaver 抽象的 AspectJ 加载时间编织的内置支持。为了方便,包含了像 context、jms 这样的自定义命名空间的介绍。测试支持扩展到了 Junit4 和 TestNG。添加了基于注解的 Spring MVC 控制器。它还支持在类路径上自动检测组件,如@Repository、@Service、@Controller 和@Conponent。现在 SimpleJdbcTemplate 支持命名 SQL 参数。包含了认证的 WebSphere 支持。还包括了对 JSR-250 注解的支持,如@Resource、PostConstruct、@PreDestroy。

3.0 GA 2009 年 12 月

整个代码已修订以支持 Java 5 特性,如泛型、可变参数。引入了 Spring 表达式语言(SpEL)。还支持 REST web 应用程序的注解。扩展了对许多 Java EE 6 特性的支持,如 JPA 2.0、JSF 2.0。版本 3.0.5 还支持 hibernate 3.6 最终版。

3.1GA 2011 年 12 月

在这个版本中,测试支持已升级到 Junit 4.9。还支持在 WebSphere 版本 7 和 8 上进行加载时间编织。

4.0 2013 年 12 月

首次全面支持 Java 8 特性。这个版本使用 Java EE 6 作为其基础。使用 Spring 4,现在可以定义使用 Groovy DSL 的外部 bean 配置。开发者现在可以将泛型类型作为一种限定符形式。@Lazy 注解可以用于注入点以及@Bean 定义。为了开发者使用基于 Java 的配置,引入了@Description 注解。引入了@Conditional 注解进行条件过滤。现在,不再需要默认构造函数供 CGLIB 基于的代理类使用。引入了@RestController 以去除对每个@RequestMapping 的@ResponseBody 的需求。包括了 AsynchRestTemplate,它允许非阻塞异步支持 REST 客户端开发。作为新的模型引入了 spring-websocket,以提供对基于 WebSocket 的服务器和客户端之间的双向通信的支持。引入了 spring-messaging 模块以支持 WebSocket 子协议 STOMP。现在可以作为元注解使用大部分来自 spring-test 模块的注解来创建自定义组合注解。org.springframework.mock.web 包中的 mocks 基于 Servlet API 3.0。

5.0 M1 Q4 2016

Spring 5M1 将支持 Java8+,但基本上,它旨在跟踪并对 Java9 的新特性提供大量支持。它还将支持响应式编程 Spring 5 将专注于 HTT2.0。它还通过响应式架构关注响应式编程。spring-aspects 中的 mock.staticmock 和 web.view.tiles2 已经被移除。不再支持 Portlet、Velocity、JasperReports、XMLBeans、JDO、Guava。

可以总结如下:

Spring 模块

容器-Spring 的心脏


POJO 开发是 Spring 框架的基石。在 Spring 中配置的 POJO,其对象的实例化、对象组装、对象管理都是由 Spring IoC 容器完成的,称为 bean 或 Spring bean。我们使用 Spring IoC,因为它遵循控制反转的模式。

控制反转(IoC)

在每一个 Java 应用程序中,每位开发者做的第一件重要的事情就是获取一个可以在应用程序中使用的对象。一个对象的状况可以在运行时获得,也可能在编译时获得。但是开发者通常在多次使用样板代码时创建对象。当同一个开发者使用 Spring 而不是亲自创建对象时,他将依赖于框架来获取对象。控制反转(IoC)这个术语是因为 Spring 容器将对象创建的责任从开发者那里反转过来。

Spring IoC 容器只是一个术语,Spring 框架提供了两个容器

  • BeanFactory

  • 应用程序上下文(ApplicationContext)

BeanFactory 的历史

BeanFactory 容器提供了基本功能和框架配置。现在,开发者不会倾向于使用 BeanFactory。那么,为什么 BeanFactory 仍然在框架中呢?为什么没有被移除呢?如果没有 BeanFactory,那么替代品是什么?让我们逐一回答这些问题。BeanFactory 在框架中的一个非常简单的答案是为了支持 JDK1.4 的向后兼容性。BeanFactory 提供了 BeanFactoryAware、InitializingBean、DisposableBean 接口,以支持与 Spring 集成的第三方框架的向后兼容性。

XMLBeanFactory

当今的企业应用程序开发需求远超过普通开发。开发者将很高兴能得到帮助来管理对象生命周期、注入依赖项或减少 IoC 容器的样板代码。XMLBeanFactory 是 BeanFactory 的常见实现。

让我们实际找出 BeanFactory 容器是如何初始化的:

  1. 创建一个名为Ch01_Container_Initialization的 Java 应用程序。

  2. 按照以下快照添加 jar 包:

需要添加的 jar 包

注意

确保你使用的是 JRE 1.8,因为它 是 Spring5.0.0.M1 的基础。你可以从...............下载 jar 包。

  1. com.ch01.test包下创建一个名为TestBeanFactory的类。

  2. 在类路径中创建一个名为beans_classpath.xml的 XML 文件,我们可以在以后编写 bean 定义。每个 bean 定义文件都包含对特定 Spring 版本的 beans.xsd 的引用。这个 XML 文件的根标签将是<beans>

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"> 
        </beans> 

我们的 XML 文件包含上述相同的代码,没有配置任何 bean。

  1. 在 main 函数中,让我们写下初始化 bean 工厂的代码,如下所示:
      BeanFactory beanFactory=new XmlBeanFactory( 
        new ClassPathResource("beans_classpath.xml")); 

在这里,bean_classpath.xml将包含 bean 定义(为了简单起见,我们没有添加任何 bean 定义,我们将在下一章详细介绍)。ClassPathResource从类路径加载资源。

  1. 有时资源不会在类路径中,而是在文件系统中。以下代码可用于从文件系统加载资源:
      BeanFactory beanFactory=new XmlBeanFactory( 
        new FileSystemResource("d:\\beans_fileSystem.xml")); 

  1. 我们需要在 D 驱动器上创建bean_fileSystem.xml,它将包含与bean_classpath.xml相同的内容。完整代码如下:
      public class TestBeanFactory { 
        public static void main(String[] args) { 
          // TODO Auto-generated method stub 
          BeanFactory beanFactory=new XmlBeanFactory(new
            ClassPathResource("beans_classpath.xml"));
          BeanFactory beanFactory1=new XmlBeanFactory(new
            FileSystemResource("d:\\beans_fileSystem.xml")); 
          System.out.println("beanfactory created successfully"); 
        } 
      } 

由于我们在这里没有编写任何输出代码,除了 Spring 容器的日志信息外,控制台上不会有任何输出。但以下快照显示了 XML 文件加载并初始化了容器:

控制台日志输出

注意

BeanFactory 不支持多个配置文件。

应用程序上下文:现状

注册 BeanProcessor 和 BeanFactoryPostProcessor 在 AOP 和属性占位符中扮演重要角色,需要显式编写代码,这使得与其配合变得不方便。开发者不想编写支持国际化的代码。处理 AOP 集成的事件发布是不可避免的。Web 应用程序需要具有特定于应用层的内容。对于这些问题,简单的解决方案是扩展 BeanFactory 提供的服务,以支持 ApplicationContext。ApplicationContext 不是 BeanFactory 的替代品,而是针对企业特定解决方案的扩展,以及为 bean 配置提供更多高级机制。

让我们来看看实现。

ClassPathXmlApplicationContext

对于独立应用程序,使用 AbstractXmlApplicationContext 的子类。它使用类路径中的配置的 bean XML 文件。如果有多个 XML 配置文件,后来的 bean 定义将覆盖先前的 bean 定义。它提供了编写新 bean 定义以替换先前的定义的优势。

让我们实际找出ClassPathXmlApplicationContext容器是如何初始化的。我们将使用相同的Ch01_Container_Initialization项目,按照以下步骤进行:

  1. com.ch01.test包下创建一个名为TestClasspathApplicationContext的类。

  2. 在新的应用程序中,像以前一样在类路径中创建一个名为beans_classpath.xml的 XML 文件。

  3. 在 main 函数中,让我们写下初始化 bean 工厂的代码,如下所示:

      try { 
        ApplicationContext context=new
          ClassPathXmlApplicationContext("beans_classpath.xml"); 
        System.out.println("container created successfully"); 
      } 
      catch (BeansException e) { 
        // TODO Auto-generated catch block 
        e.printStackTrace(); 
      } 

不需要创建 XML 文件,因为我们已经在之前的示例中创建了它。ClassPathXmlApplicationContext从类路径加载bean_classpath.xml文件,其中包含 bean 定义(为了简单起见,我们没有添加任何 bean 定义,我们将在下一章详细介绍)。

  1. 运行应用程序,输出以下内容,表明容器成功创建:

控制台输出

  1. 在 Java 企业应用程序中,项目可以有多个配置文件,因为这样可以容易地维护和支持模块化。要加载多个 bean 配置文件,我们可以使用以下代码:
      try { 
        ApplicationContext context1 = new 
          ClassPathXmlApplicationContext 
            (new String[]
            {"beans_classpath.xml","beans_classpath1.xml" }); 
       }  
       catch (BeansException e) { 
         // TODO Auto-generated catch block 
         e.printStackTrace(); 
       } 

要使用前面的代码行,我们需要在类路径中创建beans_classpath1.xml

FileSystemXmlApplicationContext

与 ClassPathXmlApplicationContext 类似,这个类也扩展了 AbstractXmlApplicationContext,用于独立应用程序。但这个类有助于从文件系统加载 bean 的 XML 定义。文件路径相对于当前工作目录。如果指定绝对文件路径,可以使用file:作为前缀。它还提供了在有多个 XML 配置的情况下,编写新的 bean 定义以替换之前的定义的优势。

让我们实际找出ClassPathXmlApplicationContext容器是如何初始化的。我们将按照以下步骤使用相同的Ch01_Container_Initialization项目:

  1. com.ch01.test包下创建一个名为TestFileSystemApplicationContext的类。

  2. 在之前应用程序中创建的 D 驱动器上创建一个新的 XML 文件beans_fileSystem.xml

  3. 在主函数中,让我们写下以下代码来初始化 bean 工厂:

      try { 
        ApplicationContext context=new   
          FileSystemXmlApplicationContext 
          ("d:\\beans_fileSystem.xml"); 
        System.out.println("container created successfully"); 
      } 
      catch (BeansException e) { 
        // TODO Auto-generated catch block 
        e.printStackTrace(); 
      } 

FileSystemXmlApplicationContext从指定路径加载bean_fileSystem.xml文件。

  1. 运行应用程序,将给出以下输出,表明容器已成功创建。

上述讨论的项目结构将如下所示:

项目目录结构

WebXmlApplicationContext

WebXmlApplicationContext继承了AbstractRefreshableWebApplicationContext。我们可以在applicationContext.xml中编写与根应用程序上下文相关的上下文定义,并将其放在 WEB-INF 下,因为这是默认的位置,从这里加载上下文定义。XXX-servlet.xml文件用于加载控制器定义,正如在 MVC 应用程序中所示。此外,我们可以通过为context-paraminit-param配置contextConfigLocation来覆盖默认位置。

容器中如何获取 bean?


是的,如果不从开发方面做任何事情,豆子或豆子对象将无法获得。Spring 管理豆子,但是必须决定要管理什么,并将其传递给容器。Spring 通过 XML 文件配置支持声明式编程。在 XML 文件中配置的豆子定义由容器加载,并使用 org.springframework.beans 在容器中实例化对象和注入属性值。豆子生命周期解释了每个豆子对象从使对象可供应用程序使用直至应用程序不再需要时将其清理并从容器中移除的各个阶段、阶段或活动。我们将在下一章讨论详细的初始化过程。

摘要


本章概述了 Spring 框架。我们讨论了在 Java 企业应用程序开发中遇到的通用问题以及 Spring 框架是如何解决它们的。我们看到了自 Spring 首次进入市场以来每个版本中发生的重大变化。Spring 框架的核心是豆子。我们使用 Spring 来简化容器管理它们的工作。我们详细讨论了两个 Spring 容器 BeanFactory 和 ApplicationContext,以及开发人员如何使用它们。容器参与了豆子生命周期管理的过程。在下一章,我们旨在深入讨论关于豆子状态管理以及一个非常著名的术语依赖注入和豆子生命周期管理。

第二章:依赖注入

上一章概述了 Spring 框架是什么以及它如何帮助开发者加快开发速度和简化开发。但是,“如何使用这个框架?”的问题仍然没有答案。在本章中,我们将从所有角度讨论答案,并尝试找出所有可能的答案。本章充满了配置和配置的替代方案。这取决于开发者如何在这些解决方案和应用程序的可用条件及环境设置中展望。我们旨在深入探讨以下几点:

  • 我们将从自定义初始化的豆子生命周期管理开始,探讨 InitializingBean、DisposableBean 和 Aware 接口,以及在豆子生命周期中使用@PostConstruct 和@PreDestroy 注解。

  • 依赖注入

  • 设置器和构造函数依赖注入

  • 依赖注入(DI)用于参考、内部豆子、继承和集合

  • 豆子作用域以及将作用域配置为单例或原型

  • 自动装配及实现自动装配的方法

  • 自动装配过程中发生的问题及解决方法

要覆盖的内容很多,所以我们从“豆子的生命周期”开始。

豆子的生命周期


Spring IoC 容器隐藏了容器与豆子之间复杂的通信。以下图表给出了容器维护每个豆子生命周期的步骤的一个大致概念:

豆子生命周期

加载配置

这是豆子生命周期中最重要的阶段,它启动生命周期过程。容器加载并从豆子配置文件中读取元数据信息,然后开始下一阶段“实例化”。

对象创建

Spring 容器使用 Java 反射 API 创建一个豆子的实例。

设置豆子名称

每个豆子都在配置中包含一个独特的名称。这个名称可以通过setBeanName()方法提供给豆子类。如果豆子类实现了BeanNameAware接口,那么它的setBeanName()方法会被调用以设置豆子名称。

设置豆子工厂

有时豆子类可能需要获取有关加载它的工厂的信息。如果豆子类实现了BeanFactoryAware,那么它的setBeanFactory()方法将被调用,传递BeanFactory实例给它,这可能是一个ApplicationContextWebApplicationContext等实例。

使用postProcessBeforeInitialization进行豆子后处理

在某些场景中,在对象的值得到填充之前需要进行一些预初始化,这无法在配置文件中完成。在这种情况下,如果 BeanPostProcessor 对象执行这个任务。BeanPostProcessor 是一种特殊的 bean,在实例化任何其他 bean 之前被实例化。这些 BeanPostProcessor bean 与容器创建的新实例交互。但是,这将分为两个步骤进行,一次是在属性设置之前,第二次是在属性设置之后。在这个阶段,与 BeanFactory 关联的 BeanPostProcessor,它的 PostProcessorBeforeInitiallization 方法将被调用进行预初始化。

属性填充

bean 配置可能指定一些 bean 属性。在这个阶段,所有值都将与在前一阶段初始化的实例相关联。

使用 bean 进行初始化

使用 afterPropertiesSet() 方法

可能发生的情况是,配置中的 bean 没有设置所有属性的值。一旦属性得到填充,使用某些业务逻辑或其他方式设置剩余的属性。InitializingBean 接口在任务中提供帮助。如果类实现了 InitializingBean 接口,其 afterPropertiesSet() 方法将调用以设置这些属性。

自定义 init() 方法

尽管 afterProperties() 有助于根据某些逻辑对属性进行初始化,但代码与 Spring API 紧密耦合。为了克服这个缺点,有一种方法可以使用自定义初始化方法来初始化 bean。如果开发者编写了自定义 init 方法并在 bean 配置中将其配置为 'init-method' 属性,容器会调用它。

使用 postProcessAfterInitialization 进行 bean 后处理

当属性初始化完成后,BeanPostProcessor 的 postProcessAfterInitialization() 方法将被调用进行 postProcessing

使用 bean

谢天谢地!!!现在对象的状态定义完美,完全准备好使用。

使用销毁 bean 的方法

开发者使用了对象,对象已经完成了它们的任务。现在我们不再需要它们了。为了释放 bean 占用的内存,可以通过以下方式销毁 bean,

使用 destroy() 方法销毁 bean

如果 bean 类实现了 DisposableBean 接口,其 destroy() 方法将被调用以释放内存。它具有与 InitializingBean 相同的缺点。为了克服这个问题,我们确实有自定义的销毁方法。

使用自定义 destroy() 方法进行销毁

也可以编写一个自定义方法来释放内存。当在 bean 配置定义中配置了 'destroy-method' 属性时,它将被调用。

  • 在了解了生命周期之后,让我们现在做一些实现,以了解实现观点。

案例 1:使用自定义初始化和销毁方法

正如我们在 bean 生命周期中已经讨论过的,这两个方法将使开发者能够编写自己的初始化和销毁方法。由于开发者不受 Spring API 的耦合,他们可以利用这一点选择自己的方法签名。

让我们看看如何逐步将这些方法钩入以供 Spring 容器使用:

  1. 创建一个名为 Ch02_Bean_Life_Cycle 的 Java 应用程序,并添加我们在之前项目中完成的 jar。

  2. 在 com.ch02.beans 包下创建一个名为 Demo_Custom_Init 的类,如下所示:

      public class Demo_Custom_Init { 
        private String message; 
        private String name; 

        public Demo_Custom_Init() { 
          // TODO Auto-generated constructor stub 
          System.out.println(""constructor gets called for  
            initializing data members in Custom init""); 
          message=""welcome!!!""; 
          name=""no name""; 
        } 

        @Override 
        public String toString() { 
          // TODO Auto-generated method stub 
          return message+""\t""+name; 
        } 
      } 

  1. 向类中添加一个名为 myInit()的方法,并使用以下代码进行初始化。在这里,我们将'name'转换为大写:
        public void myInit() 
        { 
          name=name.toUpperCase(); 
          System.out.println(""myInit() get called""); 
        } 

  1. 在类路径创建 bean_lifecycle.xml 文件,以配置类似于之前项目的 bean(参考 Ch01_Container_Initizatization 中的 beans_classpath.xml)。

  2. 在此基础上添加 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"" 
        xsi:schemaLocation= 
          "http://www.springframework.org/schema/beans 
          http://www.springframework.org/schema/beans/
          spring-beans.xsd"> 

        <bean id="obj" class="com.ch02.beans.demo_Custom_Init"></bean> 
      </beans> 

每个 bean 必须在<bean>标签内配置。

  • <bean>标签包含许多我们需要配置的属性,其中至少需要两个,如下所示:
  • a. id:指定容器识别其正在管理的对象的确切名称。'id'必须在容器内唯一。命名'id'与 Java 应用程序中的引用类似。

  • b. class:指定容器正在创建和管理哪个对象。class 属性的值必须是完全限定类名,正如我们在上面的配置中所做的那样。

  • 在 XML 中配置 bean 定义的语法如下所示:

      <bean id="id_to_use" class="fully_qualified_class_name"></bean>
  • XML 配置与 Java 代码相当,
      Demo_Custom_Init obj= new  com.ch02.beans.Demo_Custom_Init();

注意

开发者可以在配置中使用一些其他属性。我们将在接下来的章节中根据场景逐一介绍它们。

  1. 步骤 5 中显示的配置是非常基本的配置,没有向容器提供关于如何初始化属性'name'的信息。让我们通过添加'init-method'属性来修改配置,以指定在实例化后调用以初始化属性的方法名。修改后的代码如下:
      <bean id="obj" class="com.ch02.beans.demo_Custom_Init"
        init- method=""myInit""> 
      </bean> 

  1. 我们进行初始化的方式,同样我们也可以释放资源。要使用自定义的销毁方法释放资源,我们首先需要在代码中添加如下内容:
      public void destroy() 
      { 
        name=null; 
        System.out.println(""destroy called""); 
      } 

  1. 在 bean 配置中通过指定 destroy-method 来配置销毁方法,如下面的代码所示:
      <bean id="obj" class="com.ch02.beans.demo_Custom_Init"
        init-method="myInit" destroy-method="destroy"> 
      </bean> 

  1. 创建一个名为Test_Demo_Custom_Init的 Java 程序,并添加 main 函数。像我们在第一章中一样初始化容器。并使用getBean()获取Demo_Custom_Init的实例,如下所示:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context=new
        ClassPathXmlApplicationContext("beans_lifecycle.xml");
        Demo_Custom_Init obj=(Demo_Custom_Init)context.getBean("obj"); 
        System.out.println(obj); 
      } 

  1. 代码的执行给出了以下输出:
      INFO: Loading XML bean definitions from class path resource 
      [beans_lifecycle.xml] 
      constructor gets called for initializing data members 
      myInit() get called 
      welcome!!!  NO NAME 

输出清楚地显示了 bean 的生命周期阶段,包括构造、初始化、使用和销毁。

不要对缺少'destroy called'语句感到惊讶。我们可以使用以下代码优雅地关闭容器:

((AbstractApplicationContext)context).registerShutdownHook(); 

在 main 函数中添加上述代码后,甚至'destroy called'也会作为控制台输出出现。

案例 2:使用 InitializingBean 提供初始化

我们将使用与案例 1 中开发相同的项目 Ch02_Bean_Life_Cycle。

按照以下步骤操作:

  1. 在 com.ch02.beans 包中添加一个实现 InitializingBean 接口的 Demo_InitializingBean 类,如下所示:
      public class Demo_InitializingBean implements InitializingBean { 
         private String message; 
        private String name; 

        public Demo_InitializingBean() { 
          // TODO Auto-generated constructor stub 
          System.out.println(""constructor gets called for   
            initializing data members in demo Initializing bean""); 
          message=""welcome!!!""; 
          name=""no name""; 
        } 
        @Override 
        public String toString() { 
          // TODO Auto-generated method stub 
          return message+""\t""+name; 
        } 
      } 

  1. 如下所示,覆盖 afterPropertiesSet()方法以处理属性:
      @Override 
      public void afterPropertiesSet() throws Exception { 
        // TODO Auto-generated method stub 
        name=""Mr.""+name.toUpperCase(); 
        System.out.println(""after propertiesSet got called""); 
      } 

  1. 在 bean_lifecycle.xml 中再添加一个 bean,如下所示:
      <bean id="obj_Initializing"  
        class="com.ch02.beans.Demo_InitializingBean"/> 

您可以看到,我们在这里不需要重写任何 init-method 属性,正如我们在案例 1 中所做的那样,因为 afterPropertiesSet()通过回调机制在属性设置后得到调用。

  1. 创建一个带有 main 方法的 Test_InitializingBean 类,如以下代码所示:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context=new  
        ClassPathXmlApplicationContext(""beans_lifecycle.xml""); 

        Demo_InitializingBean obj=    
          (Demo_InitializingBean)context.getBean(""obj_Initializing""); 
        System.out.println(obj);  
      } 

  1. 输出执行结果如下所示:
      INFO: Loading XML bean definitions from class path resource 
      [beans_lifecycle.xml] 
      constructor gets called for initializing data members in Custom
        init 
      myInit() get called 
      constructor gets called for initializing data members in demo        initializing bean 
      after propertiesSet got called 
      welcome!!!  Mr.NO NAME 

从上面的输出中,下划线的语句与新配置的 bean 无关。但是,由于容器对所有配置的 bean 进行初始化,它也会初始化Demo_Custom_Initbean。

案例 3:使用 DisposableBean 提供内存释放

我们将使用与案例 1 中开发相同的项目 Ch02_Bean_Life_Cycle。

按照以下步骤操作:

  1. 在 com.ch02.beans 包中添加一个实现 DisposableBean 接口的 Demo_DisposableBean 类,如下所示:
      public class Demo_DisposableBean implements DisposableBean { 
         private String message; 
        private String name; 

        public Demo_DisposableBean() { 
          // TODO Auto-generated constructor stub 
          System.out.println("constructor gets called for  
            initializing data members in Disposable Bean"); 
          message="welcome!!!"; 
          name="no name"; 
        } 

        @Override 
        public String toString() { 
          // TODO Auto-generated method stub 
          return message+""\t""+name; 
        } 
      } 

  1. 如下所示覆盖 destroy()方法以释放内存:
      @Override 
      public void destroy() throws Exception { 
        // TODO Auto-generated method stub 
        System.out.println("destroy from disposable bean get called"); 
        name=null; 
      } 

  1. 在 bean_lifecycle.xml 中再添加一个 bean,如下所示:
      <bean id="obj_Disposable"    
        class="com.ch02.beans.Demo_DisposableBean"/> 

您可以看到,我们在这里不需要重写任何 destroy-method 属性,正如我们在案例 1 中所做的那样。当包含 bean 的容器关闭时,destroy()将得到回调。

  1. 创建一个带有以下代码的 Test_DisposableBean 类:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context=new  
          ClassPathXmlApplicationContext(""beans_lifecycle.xml""); 

        Demo_DisposableBean obj=  
          (Demo_DisposableBean)context.getBean("obj_Disposable"); 
        System.out.println(obj);         
        ((AbstractApplicationContext)context).registerShutdownHook(); 
      } 

  1. 我们执行主程序后将会得到以下代码:
      INFO: Loading XML bean definitions from class path resource 
      [beans_lifecycle.xml] 
      constructor gets called for initializing data members in Custom
        init 
      myInit() get called 
      constructor gets called for initializing data members in demo         initializing bean 
      after propertiesSet got called 
      constructor gets called for initializing data members in 
        Disposable Bean 
      welcome!!!  no name 
      Sep 09, 2016 10:54:55 AM 
      org.springframework.context.support.
      ClassPathXmlApplicationContext doClose 
      INFO: Closing
      org.springframework.context.support.
      ClassPathXmlApplicationContext@1405ef7: startup date
      [Fri Sep 09  10:54:54 IST 2016]; root of context hierarchy 
      destroy from disposable bean get called destroy called 

下划线行来自 Disposable demo 的 destroy(),但正如您所见的,对于 Demo_Custom_Init 类也有自定义的 destroy()方法。

案例 4:使 bean 意识到容器

我们将使用与案例 1 中开发相同的项目 Ch02_Bean_Life_Cycle。

按照以下步骤操作:

  1. 在 com.ch02.contextaware 包中添加一个实现 ApplicationContextAware 接口的 MyBean 类。

  2. 向 bean 类中添加一个 ApplicationContext 类型的数据成员。

  3. 覆盖 setApplicationContext()方法。

  4. 添加 display()方法以获取一个 bean 并显示其属性。类将如下所示:

      public class MyBean implements ApplicationContextAware { 
        private ApplicationContext context; 

        @Override 
        public void setApplicationContext(ApplicationContext ctx)  
          throws BeansException { 
          // TODO Auto-generated method stub 
          System.out.println(""context set""); 
          this.context=ctx; 
        } 
        public void display() 
        { 
          System.out.println((Demo_InitializingBean) 
            context.getBean(""obj_Initializing"")); 
        } 
      } 

在这里,我们访问了一个不是类数据成员且未注入的其他 bean。但代码显示我们仍然可以访问其属性。

  1. bean_lifecycle.xml中再添加一个 bean,如下所示:
      <bean id=""obj_myBean"" class=""com.ch02.contextAware.MyBean""/> 

  1. 创建一个带有 main 方法的 Test_MyBean 类,如下所示:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context=new  
          ClassPathXmlApplicationContext(""beans_lifecycle.xml""); 

        MyBean obj=(MyBean)context.getBean(""obj_myBean""); 
        obj.display(); 
      } 

  1. 执行后,我们将得到以下输出:
      constructor gets called for initializing data members in Custom
      init 
      myInit() get called 
      constructor gets called for initializing data members in demo        initializing bean 
      after propertiesSet got called 
      constructor gets called for initializing data members in
      Disposable Bean 
      context set 
      welcome!!!  Mr.NO NAME 

案例 4:使用 BeanPostProcessor。

我们将使用与案例 1 中开发相同的项目 Ch02_Bean_Life_Cycle。

按照以下步骤操作:

  1. 在 com.ch02.beans 包中添加一个实现 BeanPostProcessor 的 bean 类 Demo_BeanpostProcessor。

  2. 覆盖 postProcessBeforeInitialization()方法。

  3. 覆盖 postProcessAfterInitialization()方法。

  4. 完整的类定义如下所示:

      public class Demo_BeanPostProcessor implements BeanPostProcessor
      { 
        @Override 
        public Object postProcessBeforeInitialization(Object bean,  
          String beanName) throws BeansException { 
          // TODO Auto-generated method stub 
          System.out.println("initializing bean before init:-    
            "+beanName); 
          return bean; 
        } 

        @Override 
        public Object postProcessAfterInitialization(Object bean,  
          String beanName) throws BeansException { 
          // TODO Auto-generated method stub 
          System.out.println("initializing bean after init:- 
            "+beanName); 
          return bean; 
        } 
      } 

  1. 在 bean_lifecycle.xml 中再添加一个 bean,如下所示:
      <bean id=""beanPostProcessor""  
        class=""com.ch02.processor.Demo_BeanPostProcessor""/> 

  1. 创建一个带有 main 方法的TestBeanPostProcessor类。我们不必向 bean 请求beanPostProcessor,因为它的方法在容器中的每个 bean 的 init 方法前后被调用。

  2. 编写测试代码,找出初始化过程中调用方法的顺序,如下所示:

      public class Test_BeanPostProcessor { 
        public static void main(String[] args) { 
          // TODO Auto-generated method stub 
          ApplicationContext context=new  
            ClassPathXmlApplicationContext("beans_lifecycle.xml"); 

          Demo_Custom_Init  
            obj=(Demo_Custom_Init)context.getBean(""obj""); 
          System.out.println(obj); 
        } 
      } 

  1. 输出结果如下:
      INFO: Loading XML bean definitions from class path resource
        [beans_lifecycle.xml] 
      initializing bean before init:-
      org.springframework.context.event.
      internalEventListenerProcessor 
      initializing bean after init:-
      org.springframework.context.event.internalEventListenerProcessor 
      initializing bean before init:- 
      org.springframework.context.event.internalEventListenerFactory 
      initializing bean after init:-
      org.springframework.context.event.internalEventListenerFactory 
      constructor gets called for initializing data members in Custom
      init 
      initializing bean before init:- obj 
      myInit() get called 
      initializing bean after init:-obj 
      constructor gets called for initializing data members in demo           initializing bean 
      initializing bean before init:- obj_Initializing 
      after propertiesSet got called 
      initializing bean after init:-obj_Initializing 
      constructor gets called for initializing data members in              Disposable Bean 
      initializing bean before init:- obj_Disposable 
      initializing bean after init:-obj_Disposable 
      context set 
      initializing bean before init:- obj_myBean 
      initializing bean after init:-obj_myBean 
      welcome!!!  NO NAME 

下划线的陈述是为了从容器中获取我们请求的 bean。但是找出遵循的顺序,如构造函数、postProcessBeforeInitialization方法、自定义初始化方法、postProcessAfterInitialization

注解

在一个应用中,可以配置多个BeanPostProcessors。如果 bean 实现了 Ordered 接口,可以通过设置order属性来管理它们的执行顺序。每个PostBeanProcessor的作用域是每个容器。

使用 JSR-250 注解进行 bean 生命周期管理


JSR-250 注解在 bean 生命周期中起着至关重要的作用,但我们不会直接跳过去发现它们。这样做可能会导致我们忽略一些非常重要的概念。所以放心,我们会在讨论基于 JSR 的注解时讨论它们。但是,如果您已经了解 Spring,知道 Spring 中注解的使用,并且急于了解@PreDestroy 或@PostConstruct,您可以直接跳到 JSR 注解的主题。

在大型 Java 企业应用的开发中,通过编写较小的代码单元,通常为类,可以简化开发。然后,开发者通过调用彼此的方法来重复使用它们。这种架构非常复杂且难以维护。为了调用另一个类的实例方法,理解其知识是重要的。持有另一个类对象的类被称为容器。容器持有的对象称为被包含对象。现在容器对被包含对象了如指掌。开发者现在会非常高兴,因为现在他们可以轻松地重复使用被包含对象,这简化了他们的开发工作。但现在设计中有一个非常重大的缺陷。这可以通过下面介绍的两个非常著名的术语来很好地解释:

  • 松耦合:即使被包含对象发生了变化,容器类也不会受到影响。在这种场景下,容器被称为松耦合对象。开发者总是试图编写遵循松耦合的代码。在 Java 中,可以通过接口编程来实现松耦合。接口规定了什么是合同?但它并没有指定谁和如何实现这个合同。容器类对依赖的知识了解较少,使其更加灵活。

  • 紧密耦合:当被包含对象有代码更改时,容器类需要进行更改。容器与被包含对象紧密耦合,这使得开发变得困难。

注解

尽量避免编写紧密耦合的类。

无论是松耦合还是紧耦合,我们都能得到可重用的对象。现在的问题是这些对象将如何创建。在 Java 中,对象创建可以通过两种主要方式实现,如下所示:

  • 构造函数调用。

  • 工厂提供对象

在抽象层面上,这两种方式看起来很相似,因为最终用户将获得一个对象进行使用。但这并不相同。在工厂中,依赖类负责创建对象,而在构造函数中,构造函数直接被调用。Java 应用程序以对象为中心,围绕对象进行。每个开发者尝试做的第一件事就是正确初始化对象,以便以优雅的方式处理数据和执行操作。创建每个正确初始化的对象有两个步骤:实例创建和状态初始化。由于容器将涉及这两个过程,我们应该对它们有很好的了解。所以让我们从实例创建开始。

实例创建


在 java 中,创建实例有以下两种方式:

  • 使用构造函数

  • 使用工厂方法

我不会详细说明何时使用哪种方法,因为我们都是 Java 背景,已经读过或听过很多次原因。我们将直接开始讲解如何在 Spring 框架中逐一使用它们。

使用构造函数

让我们以汽车为例,来清晰地了解容器是如何通过以下步骤创建汽车对象的:

  1. 创建 Java 应用程序 Ch02_Instance_Creation 并添加我们在上一个项目中添加的 jar 文件。

  2. 在 com.ch02.beans 包中创建一个 Car 类,具有车牌号、颜色、燃料类型、价格、平均值等数据成员。代码如下:

      class Car{ 
        private String chesis_number, color, fuel_type; 
        private long price; 
        private double average; 

        public Car() { 
          // TODO Auto-generated constructor stub 
          chesis_number=""eng00""; 
          color=""white""; 
          fuel_type=""diesel""; 
          price=570000l; 
          average=12d; 
        } 
      } 

  1. 在 Car 中添加 show(),如下所示:
      public void show() 
      { 
        System.out.println(""showing car ""+chesis_number+"" having      
          color:-""+color+""and price:-""+price); 
      } 

  1. 当开发者尝试创建对象时,代码如下:
      Car car_obj=new Car(); 

现在我们需要在 XML 文件中配置BeanDefination,它代表一个 bean 实例,以便 bean 将由 Spring 容器管理。

  1. Classpath中创建instance.xml以配置我们的 CarBeanDefination,我们需要按照如下方式进行配置:
      <bean id=""car_obj"" class=""com.ch02.beans.Car""/> 
      </beans> 

  1. 在默认包中创建带有 main 函数的 TestCar,以获取 bean 并使用业务逻辑。
  • 获取 Spring 容器的实例。我们将使用 ClassPathXmlApplicationContext,如容器初始化中所讨论的。

  • 从容器中获取 bean 实例。

代码将如下所示:

      public class TestCar { 
        public static void main(String[] args) { 
          // TODO Auto-generated method stub 
          ApplicationContext context=new      
            ClassPathXmlApplicationContext(""instance.xml""); 
          Car car=(Car)context.getBean(""car_obj""); 
          car.show(); 
        }  
      } 

输出如下所示:

图 02

从输出中可以看出,容器使用了默认构造函数来定义值。让我们通过在 Car 中添加默认构造函数来证明它,如下面的代码所示:

public Car() { 
  // TODO Auto-generated constructor stub 
  chesis_number=""eng00""; 
  color=""white""; 
  fuel_type=""diesel""; 
  price=570000l; 
  average=12d; 
} 

更新后的输出将如下所示:

使用工厂方法

在 Spring 容器中配置的 bean,其对象是通过实例或静态工厂方法创建的。

使用实例工厂方法

实例创建将通过一个非静态 bean 的方法完成。要使用该方法进行实例创建,必须配置一个 ''factory-method'' 属性。有时,也可能使用其他类来创建实例。 ''factory-bean'' 属性将与 ''factory-method'' 一起配置,以便容器用于实例创建。

让我们按照以下步骤使用工厂方法进行实例创建。我们将使用相同的 Ch02_Instance_Creation。

  1. 在 com.ch02.factory 中创建一个名为 CarFactory 的类,代码如下:
      public class CarFactory { 
        private static Car car=new Car(); 

        public Car buildCar() 
        { 
          System.out.println(""building the car ""); 
          return car; 
        } 
      } 

buildCar()方法将构建一个 Car 实例并返回它。现在,使容器了解使用上述代码的任务将由 bean 定义完成。

  1. 在 instance.xml 文件中添加两个 bean,一个用于 CarFactory,另一个用于 Car,如下所示:
      <bean id="car_factory" class="com.ch02.factory.CarFactory" /> 
      <bean id="car_obj_new" factory-bean="car_factory"
        factory-method="buildCar" /> 

属性 factory-method 指定了buildCar作为从 factory-bean 属性指定的car_factory中用于实例创建的方法。这里不需要指定 class 属性。

  1. 使用以下代码创建 TestCarFactory 带有 main 函数:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context = new    
          ClassPathXmlApplicationContext(""instance.xml""); 
        Car car = (Car) context.getBean(""car_obj_new""); 
        car.show(); 
      } 

  1. 执行后,将显示以下快照,

使用静态工厂方法

我们可以在类中定义静态方法,该方法返回对象。使用 'factory-method' 属性来指定进行实例创建的方法名称。让我们使用 Ch02_Instance_Creation 项目来使用 factory-method 属性,按照以下步骤进行。

  1. 在 com.ch02.service 包中创建一个名为 CarService 的类,如下所示:
      public class CarService { 
        private static CarService carService=new CarService(); 
        private CarService(){} 
        public static CarService createService() 
        { 
          return carService; 
        } 
        public void serve() 
        { 
          System.out.println(""car service""); 
        }  
      } 

  1. 在 XML 中添加以下配置:
      <bean id="carService" factory-method="createService"       
        class="com.ch02.service.CarService"/> 

'factory-method'指定了返回实例的方法。

  1. 在 TestCarService 中编写测试代码,如下所示:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context = new  
          ClassPathXmlApplicationContext(""instance.xml""); 
        CarService carService = (CarService)     
          context.getBean(""carService""); 
        carService.serve(); 
      } 

代码执行后,将给出以下快照所示的输出:

实例创建完成后,现在是初始化状态的时候了。Java 开发人员如下初始化状态:

car.setChessis_Number(""as123er""); 
car.setColor(""baker''s chocolate""); 
car.setFuel_Type(""Petrol""); 
car.setPrice(689067L); 
car.setAverage(21); 

这里,如果我们改变了数据成员的值,状态也会随之改变。所以很明显,汽车依赖于数据成员的值。但因为我们已经设置了它们的值,它们是代码的一部分,任何更改都需要更改代码或重新部署代码。在依赖注入中,设计是以这种方式完成的,即对象 externally 实现其状态,而不是从一段代码中硬编码它们。

依赖注入


依赖倒置原则指出两个模块之间不应该紧密耦合。模块应该通过抽象依赖,其中不指定依赖的详细信息。依赖倒置原则(DIP)有助于确保松耦合的模块化编程。依赖注入是 DIP 的实现。要了解什么是依赖注入,我们首先必须清楚地了解什么是依赖?

对象的状态由其数据成员的值给出。正如我们所知,这些数据成员可以是原始类型或次要类型。如果数据成员是原始类型,它们直接获得它们的值,而在次要数据类型中,值依赖于该对象的状态。这意味着每当对象初始化发生时,数据成员初始化起着非常重要的作用。换句话说,我们可以说数据成员是对象初始化的依赖关系。将依赖关系的值插入或设置到对象中是依赖注入。

依赖注入有助于实现松耦合的架构。松耦合有助于轻松测试模块。代码及其使用的值被分离,并可以通过中心配置来控制,这使得代码维护变得容易。由于值在外部配置中,代码不受影响,这使得迁移变得容易,且更改最小。

在 Spring 框架中,可以通过以下方式实现依赖注入:

  • 设置器注入

  • 构造函数注入

上面提到的两种方法是我们可以用于 DI 的方式,但这些可以通过许多其他方式来实现,其中包括:

  • XML 基础配置

  • 使用命名空间 ''p'' 的 XML 基础配置

  • 注解基础配置

那么让我们开始逐一探索它们

XML 基础配置

设置器注入

对象的依赖关系通过 setter 注入中的 setter 方法来满足。因此,当我们进行 setter 注入时,非常重要的一点是要有一个 bean,其数据成员将通过 setter 方法进行设置。使用 setter 注入的步骤如下:

  1. 使用标准的 Java 命名约定声明一个类及其属性。

  2. 如下配置 bean 定义 XML 中的 bean:

      <bean id="bean_id" class="fully qualified _class_name"></bean> 

  1. 上面的配置中的 bean 创建实例。它必须更新以配置属性。
  • 每个标签将配置一个数据成员

  • 每个标签接受两个值

  1. name: ''name''属性指定了开发者想要配置的数据成员的名称。

  2. value: ''value''指定了要给数据成员的值。

更新后的配置如下:

      <bean id="bean_id" class="fully qualified _class_name"> 
        <property name="name_of_property" value="value_of_property"> 
      </bean> 

如果我们有多个数据成员的值需要设置,我们需要使用多个标签。

  1. 从 Spring 容器中获取 bean,然后您就可以使用它。

首先让我们找出如何通过以下步骤使用 setter 注入配置 bean:

  1. 创建 Ch02_Dependency_Injection 作为 Java 项目。

  2. 添加所有我们在上一章中已经使用的核心 Spring 库。

  3. 在 com.ch2.beans 中创建一个 Car 类。您可以参考之前项目(Ch02_Instance_Creation)中的代码。

  • 由于我们打算使用 setter 注入来注入依赖项,因此也创建 setter 方法。

  • 添加 show()方法。

代码如下:

      public class Car { 
        private String chesis_number, color, fuel_type; 
        private long price; 
        private double average; 

        public void setChesis_number(String chesis_number) { 
          this.chesis_number = chesis_number; 
        } 

        public void setColor(String color) { 
          this.color = color; 
        } 

        public void setFuel_type(String fuel_type) { 
          this.fuel_type = fuel_type; 
        } 

        public void setPrice(long price) { 
          this.price = price; 
        } 

        public void setAverage(double average) { 
          this.average = average; 
        } 

        public void show() { 
          System.out.println("showing car "+chesis_number+" 
            having color:-"+color+"and price:-"+price); 
        } 
      } 

注意

确保在创建用于在 Spring 容器中配置的类时始终遵循 Bean 命名约定。

可以通过 getter 方法获取对象的状态。因此,开发者通常会添加 getter 和 setter。添加 getter 总是取决于应用程序的业务逻辑:

  1. 现在我们需要在 classpath 中的 beans.xml 中配置 bean 定义,表示一个 bean 定义,以便该 bean 将由 Spring 容器管理。配置如下:
      <bean id=""car_obj"" class=""com.ch02.beans.Car"" /> 

  1. 在上一步骤中,只创建了一个实例,现在我们想要设置它的属性,并通过 setter 注入依赖。代码如下:
      <bean id=""car_obj"" class=""com.ch02.beans.Car""> 
        <property name=""chesis_number"" value=""eng2012"" /> 
        <property name=""color"" value=""baker''s chocolate"" /> 
        <property name=""fule_type"" value=""petrol"" /> 
        <property name=""average"" value=""12.50"" /> 
        <property name=""price"" value=""643800"" /> 
      </bean> 

如果我们不想设置任何依赖的值,简单的事情就是不在配置中添加它。

  1. 现在我们准备使用 bean。我们需要要求容器提供一个 bean 对象。在默认包中创建一个 TestCar 类,带有 main 函数。由于依赖的外部化,我们不需要更改已经在上面的 TestCar 中完成的 main 代码。代码如下:
      public class TestCar { 
        public static void main(String[] args) { 
          // TODO Auto-generated method stub 
          ApplicationContext context=new   
            ClassPathXmlApplicationContext("beans.xml"); 
          Car car=(Car)context.getBean("car_obj"); 
          car.show(); 
        } 
      } 

  1. 执行代码后,我们将得到以下输出:

图中所示的值与我们从配置中设置的相同。

构造函数注入

在构造函数注入中,依赖关系将通过构造函数的参数或简单的参数化来实现。让我们开发一个应用程序来使用基于构造函数的依赖注入。

方法 1:没有模糊性

我们将使用以下步骤的同一个项目 Ch02_Dependency_Injection:

  1. 为 Car 添加一个参数化构造函数,代码如下:
      public Car(String chesis_number,String color, double average,
        long price, String fuel_type) { 
        // TODO Auto-generated constructor stub 
        this.chesis_number = chesis_number; 
        this.average = average; 
        this.price = price; 
        this.color=color; 
        this.fuel_type=fuel_type; 
      } 

注意

如果你添加了一个参数化构造函数,并且你想要同时使用 setter 和构造函数注入,你必须添加一个默认构造函数

  1. 在 beans.xml 文件中再添加一个 bean,但这次我们不再使用标签,而是使用来实现构造函数 DI。代码如下:
      <bean id="car_const" class="com.ch02.beans.Car"> 
        <constructor-arg value=""eng023""></constructor-arg> 
        <constructor-arg value=""green""></constructor-arg> 
        <constructor-arg value=""12""></constructor-arg> 
        <constructor-arg value=""678900""></constructor-arg> 
        <constructor-arg value=""petrol""></constructor-arg> 
      </bean> 

  1. 在默认包中创建一个 TestCarConstructorDI 类,该类将从容器中接收 Car 对象。代码如下:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context=new    
          ClassPathXmlApplicationContext(""instance.xml""); 
        Car car=(Car)context.getBean(""car_const""); 
        car.show(); 
      } 

在这里,容器对于每个参数具有不同的数据类型感到满意。但并非每次都是这样。我们可能会遇到构造函数具有多个模糊数据类型的代码。有时我们确实在类中有一个以上的构造函数,并且由于自动向上转型,容器可能会调用意外的构造函数。也可能会发生开发人员只是漏掉了参数的顺序。

方法 2:带有模糊性

  1. 让我们在 beans.xml 中添加一个如下的 bean 定义:
      <bean id=""car_const1"" class=""com.ch02.beans.Car""> 
        <constructor-arg value=""eng023""></constructor-arg> 
        <constructor-arg value=""green"" ></constructor-arg> 
        <constructor-arg value=""petrol""></constructor-arg> 
        <constructor-arg value=""12"" ></constructor-arg> 
        <constructor-arg value=""678900""></constructor-arg> 
      </bean> 

参数的数量与构造函数定义相匹配,但第三个参数传递的是 fuel_type 值,而不是平均值。不用担心,继续你的旅程,保持信念!

  1. 创建 TestConstructor_Ambiguity 以找出参数不匹配时会发生什么。代码如下:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context=new    
          ClassPathXmlApplicationContext(""beans.xml""); 
        Car car=(Car)context.getBean(""car_const1""); 
        car.show(); 
      } 

  1. 执行 main 函数时会给出异常,如下快照所示:

下划线行表达了参数值的歧义。我们可以有两个解决方案:

你可以改变配置的顺序,以便与构造函数参数相匹配。

Spring 通过提供“index”属性提供了便捷的方法来解决构造函数参数的顺序。

Spring 给出的配置“type”属性的另一种方法。

  1. 让我们尝试通过更新 Step 1 的 bean 定义来配置“index”,如下所示:
      <bean id=""car_const1"" class=""com.ch02.beans.Car""> 
        <constructor-arg value=""eng024"" index=""0"">
          </constructor-arg> 
        <constructor-arg value=""yellow"" index=""1"">
          </constructor-arg> 
        <constructor-arg value=""petrol"" index=""4"">
          </constructor-arg> 
        <constructor-arg value=""15"" index=""2"">
          </constructor-arg> 
        <constructor-arg value=""688900"" index=""3"">
          </constructor-arg> 
      </bean> 

你会发现我们这次配置了一个额外的属性作为“index”,这将告诉容器哪个值对应哪个参数。“index”总是从“0”开始。我们没有改变配置中属性的顺序。

  1. 运行相同的 TestConstructor_Ambiguity。你将没有任何问题地得到你的实例。

注意

指定索引是克服歧义的最安全方式,但即使我们可以用“类型”代替索引来解决问题。但是,如果构造函数有两个以上相同数据类型“类型”将无法帮助我们。要在我们的 previous code 中使用 type,我们需要在 XML 文件中更改 bean 定义,如下所示:

在这里我们探讨了设置豆子属性的方法。但是,如果你敏锐地观察我们在这里设置的所有属性都是原始的,但我们甚至可以将次要数据作为数据成员。让我们找出如何借助以下示例来设置次要数据类型的属性。

让我们开发一个 Customer 的示例,它有一个 Address 作为数据成员。为了更好地理解,请按照以下步骤操作:

  1. 创建 Java 应用程序,“Ch02_Reference_DI”,并添加与之相关的 jar 文件,就像之前的示例一样。

  2. 在 com.ch02.beans 包中创建 Address 类,具有 city_name、build_no、pin_code 作为数据成员。也为它添加 setter 方法。代码如下:

      public class Address { 
        private String city_name; 
        private int build_no; 
        private long pin_code; 

        public void setCity_name(String city_name) { 
          this.city_name = city_name; 
        } 
        public void setBuild_no(int build_no) { 
          this.build_no = build_no; 
        } 
        public void setPin_code(long pin_code) { 
          this.pin_code = pin_code; 
        } 
        @Override 
        public String toString() { 
          // TODO Auto-generated method stub 
          return this.city_name+""\t""+this.pin_code; 
        } 
      } 

  1. 在 com.ch02.beans 包中创建 Customer,具有 cust_name、cust_id、cust_address。添加 getter 和 setter 方法。代码如下:
      public class Customer { 
        private String cust_name; 
        private int cust_id; 
        private Address cust_address; 
        //getter and setter methods 
      } 

你可以很容易地发现 cus_address 是次要数据。

  1. 对于配置,在 Classpath 中创建 customer.xml。

  2. 在 XML 中我们需要配置两个 bean。第一个 bean 是 Address,第二个是 Customer。首先让我们如下配置 Address bean:

      <bean id=""cust_address "" class=""com.ch02.beans.Address""> 
        <property name=""build_no"" value=""2"" /> 
        <property name=""city_name"" value=""Pune"" /> 
        <property name=""pin_code"" value=""123"" /> 
      </bean> 

注意我们在这里使用的 Address 的 id 是“cust_address”,但如果你愿意,你可以使用自己的。

  1. 现在,请按照以下代码添加 Customer 的配置:
      <bean id=""customer"" class=""com.ch02.beans.Customer""> 
        <property name=""cust_id"" value=""20"" /> 
        <property name=""cust_name"" value=""bob"" /> 
        <property name=""cust_address"" ref=""cust_address"" /> 
      </bean> 

cust_id 和 cust_name 将直接具有值属性。但是,cust_address 不是原始数据,因此我们在这里需要使用“ref”而不是“value”作为属性。

ref: “ref”属性用于引用我们需要注入的对象。 “ref”的值是容器中“id”的值。这里我们使用 ref 值作为“cust_address”,因为我们已经为 Address 数据类型声明了一个具有相似 id 的 bean。

  1. 现在是测试代码运行情况的时候了。在默认包中添加 TestCustomer,带有 main 方法,使用以下代码从容器中获取 Customer 对象:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context=new  
          ClassPathXmlApplicationContext("customer.xml"); 
        Customer customer=(Customer)context.getBean("customer"); 
        System.out.println(customer.getCust_name()+"\t"+ 
          customer.getCust_id()); 
      } 

注意

开发人员通常会调用 bean 的业务逻辑方法,而不是直接使用数据成员。

  1. 执行后,我们将在控制台上得到客户 id 和客户名称。

注意

我们甚至可以在这里使用构造函数注入,通过添加参数化构造函数,并使用代替标签。如上所述,'value'属性必须替换为'ref'。您可以从 Customer_Constructor_DI.java 找到代码。

命名空间'p'用于属性

在之前的某个例子中,我们曾经使用标签来注入属性的值。框架提供了更复杂且替代的'p'命名空间方式。要使用'p'命名空间,开发者必须在配置文件中添加模式 URI www.springframework.org/schema/p,如下所示:

xmlns:p=""http://www.springframework.org/schma/p""  

使用'p'设置属性基本值的语法如下:

p: name_of_property =value_of_the_property_to_set. 

设置多个属性时,用空格将它们分隔开。

引用数据类型的语法如下变化:

p: name_of_property-ref =id_of_bean_which_to_inject 

让我们在 XML 中使用新的配置。我们将使用与 Ch02_Dependency_Injection 相同的项目结构,只需修改 beans.xml。让我们按照以下步骤创建一个新的应用程序:

  1. 创建一个名为 Ch02_DI_using_namespce 的 Java 项目,并将其中的 jar 文件添加进去。

  2. 在 com.ch02.beans 包中创建或复制 Car 类。您可以参考 Ch02_Dependency_Injection 的代码。

  3. 创建 beans.xml 文件并更新它,以声明命名空间'p',如下所示。配置文件将如下所示:

      <beans xmlns="http://www.springframework.org/schema/beans"    
        xmlns:p="http://www.springframework.org/schema/p"  
        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"">  
      </beans> 

  1. 使用命名空间'p'添加 bean 定义,如下所示:
      <bean id="car_obj" class="com.ch02.beans.Car"    
        p:chesis_number="eng2013" p:color="baker's chocolate"    
        p:fuel_type="petrol" p:average="12.50" p:price="750070"> 
      </bean> 

  1. 创建一个 TestCar 类,以从容器中获取 Car 实例并执行代码。您可以参考 Ch02_Dependency_Injection 项目中的 TestCar.Java。

一旦我们知道了如何设置基本类型,接下来让我们也为引用配置编写代码。我们将使用同一个 DI_using-namespce 项目来开发后续的代码:

  1. 复制或创建 Address 类。(参考 Ch02_Reference_DI 中的代码)在 com.ch02.beans 包中。

  2. 复制或创建 Customer 类。(参考 Ch02_Reference_DI 中的代码)在 com.ch02.beans 包中。

  3. 在 bean.xml 中使用 setter DI 添加一个 Address bean。

  4. 使用命名空间'p'添加一个 Customer 的 bean,配置将如下所示:

      <bean id=""customer"" class=""com.ch02.beans.Customer""  
         p:cust_id=""2013"" p:cust_name=""It''s Bob""  
         p:cust_address-ref=""cust_address""> 
      </bean> 

您可以看到,这里客户的地址不是基本类型,因此我们不是使用值,而是使用引用作为 p:cust_address ="cust_address",其中'cust_address'是表示 Address bean 的 id。

  1. 创建一个 TestCustomer 类,其中包含以下代码:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context=new  
          ClassPathXmlApplicationContext(""beans.xml""); 
        Customer customer=(Customer)context.getBean(""customer""); 
        System.out.println(customer.getCust_name()+""\t""+ 
          customer.getCust_id()); 
      } 

  1. 代码执行将显示以下输出:

配置内部 bean

bean 定义文件中有多个由容器管理的 bean。我们都知道这些 bean 可以互相复用。这一点令人惊叹,但随之而来的问题是。让我们以顾客的地址为例。可以为地址和顾客进行配置。我们可以使用地址 bean 为另一个顾客吗?是的,我们可以。但是......如果顾客不想分享他的地址并将其保留为私人信息呢?当前的配置是不可能的。但我们有一个替代配置,可以保留地址的私人信息。从 Ch02_Reference_DI 中修改顾客 bean 的配置以使用内部如下:

<bean id=""customer_obj"" class=""com.ch02.beans.Customer""> 
  <property name=""cust_id"" value=""20"" /> 
  <property name=""cust_name"" value=""bob"" /> 
  <property name=""cust_address""> 
    <bean class=""com.ch02.beans.Address""> 
      <property name=""build_no"" value=""2"" /> 
      <property name=""city_name"" value=""Pune"" /> 
      <property name=""pin_code"" value=""123"" /> 
    </bean> 
  </property> 
</bean> 

地址 bean 是一个内部 bean,其实例将由 Customer 实例的 address 属性创建并连接。这与 Java 内部类相似。

由于 setter 注入支持内部 bean,它们也由构造函数注入支持。配置如下:

<bean id=""customer_obj_const"" class=""com.ch02.beans.Customer""> 
  <constructor-arg value=""20"" /> 
  <constructor-arg value=""bob"" /> 
  <constructor-arg> 
    <bean id=""cust_address"" class=""com.ch02.beans.Address""> 
      <property name=""build_no"" value=""2"" /> 
      <property name=""city_name"" value=""Pune"" /> 
      <property name=""pin_code"" value=""123"" /> 
    </bean> 
  </constructor-arg> 
</bean> 

你可以在中国 Ch02_InnerBeans 项目中找到完整的代码。

继承映射

继承是 Java 的支柱。Spring 支持在 XML 中配置 bean 定义。继承支持是由'parent'属性提供的,它表示缺少的属性可以从父 bean 中使用。使用'parent'在继承中开发 Java 代码时与父类相似。即使它也允许覆盖父 bean 的属性。让我们通过以下步骤找出如何实际使用:

  1. 创建一个名为 Ch02_Inheritance 的 Java 项目。

  2. 添加 jar 文件。

  3. 在 com.ch02.beans 包中创建一个 Student 类,代码如下:

      public class Student { 
        private int rollNo; 
        private String name; 
        // getters and setters 
      } 

  1. 在 com.ch02.beans 包中创建一个继承自 Student 的EnggStudent类:
      public class EnggStudent extends Student { 
        private String branch_code; 
        private String college_code; 
        // getters and setters 
      } 

  1. 在类路径中创建 student.xml,以配置 student 的 bean,如下所示:
      <bean id=""student"" class=""com.ch02.beans.Student""> 
        <property name=""rollNo"" value=""34"" /> 
        <property name=""name"" value=""Sasha"" /> 
      </bean> 

  1. 现在该为 EnggStudent 配置 bean 了。首先是一个我们不想要使用的普通配置:
      <bean id=""engg_old"" class=""com.ch02.beans.EnggStudent""> 
        <property name=""rollNo"" value=""34"" /> 
        <property name=""name"" value=""Sasha"" /> 
        <property name=""branch_code"" value=""Comp230"" /> 
        <property name=""college_code"" value=""Clg_Eng_045"" /> 
      </bean> 

很明显,我们重复了 rollNo 和 name 的配置。我们不需要通过配置'parent'属性来重复配置,如下所示:

<bean id="engg_new" class="com.ch02.beans.EnggStudent"  
  parent="student"> 
  <property name="branch_code" value="Comp230"/> 
  <property name=""college_code"" value=""Clg_Eng_045"" /> 
</bean> 

虽然这里我们跳过了配置 name 和 rollNo 并从'student'bean 中重用它,但如下所示,重写其中的任何一个都是可能的:

<bean id="engg_new1" class="com.ch02.beans.EnggStudent"   
  parent="student"> 
  <property name=""rollNo"" value=""40"" /> 
  <property name=""branch_code"" value=""Comp230"" /> 
  <property name=""college_code"" value=""Clg_Eng_045"" /> 
</bean> 

选择权在你,使用哪一个!!

  1. 编写带有 main 函数的 TestStudent 如下:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context=new  
          ClassPathXmlApplicationContext("student.xml"); 
        //old configuration 
        EnggStudent student_old= 
          (EnggStudent)context.getBean("engg_old"); 
        System.out.println(""with old configuration""); 
        System.out.println(student_old.getName()+ 
          "\t"+student_old.getRollNo()+"\t"+ 
          student_old.getCollege_code()); 
        //new configuration 
        EnggStudent student_new=
          (EnggStudent)context.getBean(""engg_new""); 
        System.out.println(""with new configuration""); 
        System.out.println(student_new.getName()+ 
          "\t"+student_new.getRollNo()+"\t"+ 
          student_new.getCollege_code()); 
        System.out.println(""with new configuration with       
          overridding roll number""); 
        //new configuration with overriding the roll number 
        EnggStudent student_new1=
          (EnggStudent)context.getBean(""engg_new1""); 
        System.out.println(student_new1.getName()+"\t"+ 
          student_new1.getRollNo()+"\t"+ 
          student_new1.getCollege_code()); 
      } 

  1. 下面的快照显示了控制台输出如下:

  1. 开发者可以获得 Student 实例以及 EnggStudent 实例。在某些情况下,我们不希望任何人使用 Student 实例,或者没有人应该能够获得 Student 实例。在这种情况下,在不想公开可用的 bean 实例上配置属性'abstract'。默认情况下,abstract 的值为 false,表示任何人都可以获得 bean 的实例。我们将配置 abstract 为"true",使其不可用。Student 的更新配置如下:
      <bean id=""student"" class=""com.ch02.beans.Student""    
        abstract=""true""> 
        <property name=""rollNo"" value=""34"" /> 
        <property name=""name"" value=""Sasha"" /> 
      </bean>
  1. 现在,无论何时有人要求 student bean,BeanIsAbstractException 将被抛出。您可以通过在 TestStudent 中添加以下行来尝试代码:
      System.out.println(""obtaining Student instance""); 
      Student student=(Student)context.getBean(""student""); 

  1. 在执行时,我们将得到以下堆栈跟踪,它指定了在获取 Student bean 时发生的 bean 创建异常:

配置空值

在 Java 中,除非任何数据成员的值没有被设置,否则每个都会得到它们的默认值。引用属性将被定义为 null,基本整数属性将分别被设置为'0'。这些 null 稍后将通过构造函数注入或 setter 注入被覆盖。也可能是开发人员希望保持它为 null,直到某些业务逻辑不会给出计算值或从某些外部资源获取它。无论什么原因,我们想要配置值为 null,只需使用''。如以下所示,将 Customer 的地址设置为 null:

<bean id=""customer_obj"" class=""com.ch02.beans.Customer""> 
  <property name=""cust_id"" value=""20"" /> 
  <property name=""cust_name"" value=""bob"" /> 
  <property name=""cust_address""><null/></property> 
</bean> 

您可以在 Ch02_InnerBeans 的 customer.xml 中找到配置。

到目前为止,我们已经看到了基本数据类型、引用、空值或内部 bean 的映射。我们非常高兴,但等待一个非常重要的 Java 基本概念——集合框架。是的,我们还需要讨论集合的映射。

配置集合

Java 中的集合框架使得以简化方式处理对象以执行各种操作成为可能,如添加、删除、搜索、排序对象。Set、List、Map、Queue 等接口有许多实现,如 HashSet、ArrayList、TreeMap、PriorityQueue 等,为处理数据提供了手段。我们不会详细讨论选择哪种操作,但我们将会讨论 Spring 支持的不同配置,以注入集合。

映射列表

列表是有序的集合,它按数据插入顺序处理数据。它维护插入、删除、通过索引获取数据,其中允许重复元素。ArrayList、LinkedList 是其一些实现。框架支持使用标签配置 List。让我们按照以下步骤开发一个应用程序来配置 List:

  1. 创建 Ch02_DI_Collection 作为 Java 项目,并添加 Spring jars。

  2. 在 com.ch02.beans 包中创建一个 POJO 类 Book。

  • 添加 isbn、book_name、price 和 publication 作为数据成员。

  • 添加默认和参数化构造函数。

  • 编写.getter 和 setter 方法。

  • 由于我们正在处理集合,所以添加equals()hashCode()

  • 为了显示对象,添加toString()

书籍的内容将如下面的代码所示,

      public class Book { 
        private String isbn; 
        private String book_name; 
        private int price; 
        private String publication; 

        public Book() 
        { 
          isbn=""310IND""; 
          book_name=""unknown""; 
          price=300; 
          publication=""publication1""; 
        }       

        public Book(String isbn,String book_name,int price, String  
          publication) 
        { 
          this.isbn=isbn; 
          this.book_name=book_name; 
          this.price=price; 
          this.publication=publication; 
        } 

        @Override 
        public String toString() { 
          // TODO Auto-generated method stub 
          return book_name+""\t""+isbn+""\t""+price+""\t""+publication; 
        } 

        @Override 
        public boolean equals(Object object) { 
          // TODO Auto-generated method stub 
          Book book=(Book)object; 
          return this.isbn.equals(book.getIsbn()); 
        } 

        public int hashCode() { 
          return book_name.length()/2; 
        } 

        // getters and setters 
      } 

  1. 在 com.ch02.beans 中创建 Library_List,它有一个 Book 列表。编写 displayBooks()以显示书籍列表。代码将是:
      public class Library_List { 
        private List<Book> books; 
        public void displayBooks() 
        { 
          for(Book b:books) 
          { 
            System.out.println(b); 
          } 
        } 
        // getter and setter for books 
      } 

  1. 在 ClassPath 中创建 books.xml,并向其中添加四个 Book bean。我尝试了以下三种配置:
  • 使用 setter DI 创建 book bean。

  • 使用构造函数 DI 创建 book bean。

  • 使用''p''命名空间创建 book bean。

无需尝试所有组合,你可以跟随其中任何一个。我们已经在之前的演示中使用了它们全部。配置将如下所示:

     <bean id=""book1"" class=""com.ch02.beans.Book""> 
       <property name=""isbn"" value=""20Novel"" /> 
       <property name=""book_name"" value=""princess Sindrella"" /> 
       <property name=""price"" value=""300"" /> 
       <property name=""publication"" value=""Packt-Pub""></property> 
     </bean> 

     <bean id="book2" class="com.ch02.beans.Book"> 
       <constructor-arg value="Java490" /> 
       <constructor-arg value="Core Java" /> 
       <constructor-arg value="300" /> 
       <constructor-arg value="Packt-Pub" /> 
     </bean> 

     <bean id="book3" class="com.ch02.beans.Book"   
       p:isbn="200Autobiography"
       p:book_name="Playing it in my way" p:price="300"  
       p:publication="Packt-Pub"> 
     </bean> 

故意第四本书是前三本书中的其中一本的第二份,我们在接下来的几个步骤中发现原因。敬请期待!

  1. 配置中添加一个 Library bean:
      <bean id=""library_list"" class=""com.ch02.beans.Library_List""> 
        <property name=""books""> 
          <list> 
            <ref bean=""book1""></ref> 
            <ref bean=""book2""></ref> 
            <ref bean=""book3""></ref> 
            <ref bean=""book3""></ref> 
          </list> 
        </property> 
      </bean> 

包含 的列表,用于注入书的列表,其中 'book1'、'book2'、'book3'、'book4' 是我们在第 4 步创建的 bean 的 id。

  1. 创建一个带有 main 函数的 TestLibrary_List 以获取 Library 实例和它包含的 Book 列表。代码如下:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context=new   
          ClassPathXmlApplicationContext(""books.xml""); 
        Library_List  list=       
          (Library_List)context.getBean(""library_list""); 
        list.displayBooks(); 
      } 

  1. 执行它以显示书目列表的输出。找到最后两个条目,这表明 List 允许重复元素:

映射集

Set 接口是一个无序集合,该集合不允许集合中有重复条目。HashSet、TreeSet 是 Set 的实现。Spring 提供了 标签来配置 Set。让我们使用 Ch02_DI_Collection 项目按照以下步骤添加书的 Set:

  1. 在 com.ch02.beans 包中添加 Library_Set 类并声明 Set 的书作为数据成员。为它添加 getter 和 setter。代码如下所示:
      public class Library_Set { 
        HashSet<Book> books; 

        public void displayBooks() 
        { 
          for(Book b:books) 
          { 
            System.out.println(b); 
          } 
        } 
        //getter and setter for books 
      } 

  1. 在 beans.xml 中为 Library_Set 添加一个 bean,并使用如下 配置:
      <bean id=""library_set"" class=""com.ch02.beans.Library_Set""> 
        <property name=""books""> 
          <set> 
            <ref bean=""book1""></ref> 
            <ref bean=""book2""></ref> 
            <ref bean=""book3""></ref> 
            <ref bean=""book3""></ref> 
          </set> 
        </property> 
      </bean> 

  1. 创建一个带有 main 函数的 TestLibrary_Set,如下所示:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context=new  
          ClassPathXmlApplicationContext(""books.xml""); 
        Library_Set set=     
          (Library_Set)context.getBean(""library_set""); 
        set.displayBooks();      
      } 

  1. 执行结果如下所示:

我们注入了四本图书的对象,但输出中只有三本,所以很明显 Set 不允许重复项。

映射 Map

Map 处理具有键值对的对象集合。Map 可以有重复的值,但不允许重复的键。它的实现包括 TreeMapHashMapLinkedHashMap

让我们通过以下步骤探索配置 Map:

  1. 我们将使用同一个 Ch02_DI_Collection 项目。在 com.ch02.beans 包中创建 Library_Map 类。

  2. 在其中添加一个 <Map<String,Book> 类型的 books 数据成员,并添加相应的 getter 和 setter。不要忘记在其中添加 displayBooks()。代码如下所示:

      public class Library_Map { 
        private Map<String,Book> books; 
        //getters and setters 

        public void displayBooks() 
        { 
          Set<Entry<String, Book>> entries=books.entrySet(); 
          for(Entry<String, Book> entry:entries) 
          { 
            System.out.println(entry.getValue()); 
          }    
        }   
      } 

  1. 如下配置 Map 在 beans.xml 中:
<bean id=""library_map"" class=""com.ch02.beans.Library_Map""> 
    <property name=""books""> 
      <map> 
        <entry key=""20Novel"" value-ref=""book1"" /> 
        <entry key=""200Autobiography"" value-ref=""book3"" /> 
        <entry key=""Java490"" value-ref=""book2"" /> 
      </map> 
    </property> 
  </bean> 

与 List 和 Set 不同,Map 需要一个额外的 'key' 属性来指定键。我们这里用书名作为键,但如果你愿意,也可以声明其他东西。只是不要忘记在 Map 中键总是唯一的:

  1. 像下面这样编写 TestLibrary_Map 主函数:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context=new     
          ClassPathXmlApplicationContext("books.xml"); 
        Library_Map map=  
          (Library_Map)context.getBean("library_map"); 
        map.displayBooks(); 
      } 

  1. 执行代码以在控制台上显示书目详情。

这里我们为每个条目配置单个对象。但在实际中,一个条目可能包含 List 或 Set 的书。在这种情况下,配置将包含 如下:

<bean id=""library_map1"" class=""com.ch02.beans.Library_Map1""> 
    <property name=""books""> 
      <map> 
        <entry key=""20Novel""> 
          <list> 
            <ref bean=""book1""></ref> 
            <ref bean=""book1""></ref> 
          </list> 
        </entry> 
        <entry key=""200Autobiography""> 
          <list> 
            <ref bean=""book3""></ref> 
            <ref bean=""book3""></ref> 
          </list> 
        </entry> 
      </map> 
    </property> 
  </bean> 

在上述配置中,每个 ''条目'' 都有一个包含 '''' 的书名,这显然是我们谈论的是图书目录。Library_Map1.java 和 TestLibrary_Map1.java 的完整代码可以在 Ch02_DI_Collection 中查阅。

映射属性

属性也包含键值对的集合,但与 Map 不同,这里键和值都是 String 类型。属性可以从流中保存或读取。

考虑一个拥有多个州作为属性的国家。按照以下步骤找出如何在beans.xml中配置属性:

  1. 在 com.ch02.beans 包中创建一个 Country 类。

  2. 将 name, continent 和 state_capital 声明为数据成员。添加 getters 和 setters。为了显示州首府,添加 printCapital()。代码如下所示:

      public class Country { 
        private String name; 
        private String continent; 
        private Properties state_capitals; 

        // getters and setter  

        public void printCapitals() 
        { 
          for(String state:state_capitals.stringPropertyNames()) 
          {   
            System.out.println(state+"":\t""+ 
              state_capitals.getProperty(state)); 
          } 
        } 
      } 

  1. 在 beans.xml 中如下配置 Country 的定义:
      <bean id=""country"" class=""com.ch02.beans.Country""> 
        <property name=""name"" value=""India""></property> 
        <property name=""continent"" value=""Asia""></property> 
        <property name=""state_capitals""> 
          <props> 
            <prop key=""Maharashtra"">Mumbai</prop> 
            <prop key=""Goa"">Panji</prop> 
            <prop key=""Punjab"">Chandigad</prop> 
          </props> 
        </property> 
      </bean> 

'state_capitals'包含<props>配置,以'key'状态名称和'value'其首府作为键值对。

  1. 编写带有以下代码的 TestProperties 主函数,
public static void main(String[] args) { 
    // TODO Auto-generated method stub 
    ApplicationContext context=new   
                   ClassPathXmlApplicationContext(""books.xml""); 
    Country country=(Country)context.getBean(""country""); 
    country.printCapitals();; 
   } 

  1. 输出将如快照所示:

'util'命名空间为开发者在 XML 文件中优雅地配置集合提供了一种手段。使用'util'命名空间,可以配置 List、Map、Set、Properties。要使用'util'命名空间,必须更新www.springframework.org/schma/util URI 的架构,如下快照所示:

下划线的行必须添加以在配置中使用'util'命名空间。

使用''util''命名空间的 List 配置将如下所示:

      <bean id=""library_list1"" class=""com.ch02.beans.Library_List""> 
        <property name=""books""> 
          <util:list> 
            <ref bean=""book1""></ref> 
            <ref bean=""book2""></ref> 
            <ref bean=""book3""></ref> 
          </util:list> 
        </property> 
      </bean> 

您可以在 books.xml 中找到更新的配置。

我们知道如何获取 bean 以及如何满足它具有的不同类型的依赖关系。bean 配置定义了实例的创建方式及其状态将由注入依赖项来定义。在任何时刻,根据业务逻辑需求,bean 的状态可以更改。但我们还不知道 Spring 容器创建了多少实例,或者如果开发者希望每个请求都使用单个实例该怎么办?或者每个操作都需要不同的实例。实际上我们在谈论“作用域”

Bean 作用域

bean 的作用域定义了 Spring 容器将创建多少实例,并使其可供应用程序使用。使用<bean>scope属性提供关于实例数量的信息。在了解创建和提供实例的默认过程之前,我们无法前进。这将使“作用域”一词变得清晰,并且还将定义为什么理解“作用域”如此重要。

让我们使用 Ch02_Dependency_Injection 项目来找出容器默认创建多少实例。您可以使用同一个项目,或者可以创建一个新的副本,如下步骤所示。

  1. 创建 Ch02_Bean_Scope 作为 Java 项目。

  2. 向其添加 Spring jar。

  3. 在 com.ch02.beans 包中创建或复制 Car 类。

  4. 在类路径中创建 beans.xml,并如下配置'car' bean,

      <bean id=""car"" class=""com.ch02.beans.Car""> 
        <property name=""chesis_number"" value=""eng2012"" /> 
        <property name=""color"" value=""baker''s chocolate"" /> 
        <property name=""fuel_type"" value=""petrol"" /> 
        <property name=""average"" value=""12.50"" /> 
        <property name=""price"" value=""643800"" /> 
      </bean> 

作用域与 bean 是如何配置的无关。

  1. 创建 TestCar 以如下的方式请求两次'car' bean。不要感到惊讶。我们想找出创建了多少实例。所以让我们开始吧:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context=new  
          ClassPathXmlApplicationContext(""beans.xml""); 
        // first request to get the car instance 
        Car car_one=(Car)context.getBean(""car""); 
        car_one.show(); 

        //second request to get the car instance 
        Car car_two=(Car)context.getBean(""car""); 
        car_two.show(); 
      } 

代码执行将给出以下输出,

是的,两个对象有相同的值。为什么不呢?我们在 XML 中配置它们。这不会导致我们得出任何结论。让我们进行第二步。

  • 使用相同的 TestCar 代码,但这次改变任意一个 car 对象的状态。我们将为“car_one”进行更改,并观察 car_two 会发生什么?car_two 将包含更改后的值还是配置的值?更改后的代码如下:
      public class TestCarNew { 
        public static void main(String[] args) { 
          // TODO Auto-generated method stub 
          ApplicationContext context=new  
            ClassPathXmlApplicationContext(""beans.xml""); 
          // first request to get the car instance 
            Car car_one=(Car)context.getBean(""car""); 
            car_one.show(); 

          //second request to get the car instance 
            Car car_two=(Car)context.getBean(""car""); 
            car_two.show(); 

          System.out.println(""after changing car-one is""); 
          // change average and color of car_one 
          car_one.setAverage(20.20d); 
          car_one.setColor(""white""); 
          car_one.show(); 
          System.out.println(""after changing car-two is""); 
          car_two.show(); 
        } 
      } 

执行后,你将得到以下输出。

我们只是改变了 car_one 的状态,但输出显示 car_two 的状态也发生了变化,这证明无论你多少次向容器请求实例,每次都会返回同一个实例。

注意

singleton是 bean 的默认作用域,意味着每个 Spring 容器有一个实例。

  • 保持 TestCarNew 不变,并在 car bean 中配置scope属性,如下所示,
<bean id=""car"" class=""com.ch02.beans.Car"" scope=""prototype""> 

执行 TestCarNew,你将得到以下输出:

输出显示,car_one 的状态变化不会改变 car_two 的状态。这意味着,每次从容器中请求 car 实例时,容器都会创建并给予一个新的实例。

注意

prototype指定每次从容器请求实例时都创建一个新的实例。

以下是一些由 Spring 框架给出的作用域,

  • 请求:在 Web 应用程序中,默认情况下所有 HTTP 请求都由同一个实例处理,这可能导致处理数据或数据一致性问题。为了确保每个请求都能获得自己全新的实例,请求作用域将会有帮助。

  • 会话:我们很清楚在 Web 应用程序中处理会话。每个请求创建一个实例,但当多个请求绑定到同一个会话时,每个请求的实例变得杂乱无章,管理数据也是如此。在需要按会话创建实例的情况下,会话作用域是拯救之策。

  • 应用程序:每个 Web 应用程序都有自己的 ServletContext。应用程序作用域创建每个 ServletContext 的实例。

  • 全局会话:在 protletContext 中,Spring 配置提供每个全局 HTTPSession 的实例。

  • WebSocket:WebSocket 作用域指定每个 WebSocket 创建一个实例。

基于注解的配置

从 Spring 2.5 开始,Spring 开始支持基于注解的配置。我们已经讨论了在 Java 中的概念以及如何在 XML 中进行配置?在执行基于注解的配置时,我们会遇到两种类型的配置

  • 基于 Spring 的注解

  • 基于 JSR 的注解。

基于 Spring 的注解

Spring 提供了许多注解,可以分为管理生命周期、创建 bean 实例、注解配置等类别。让我们逐一找到它们。但在那之前,我们需要知道一个非常重要的事情。bean 定义必须通过在 XML 中配置来注册。现在,为了实现基于注解的配置,bean 注册必须使用 context 命名空间如下进行:

<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"" 
  xsi:schemaLocation= 
    "http://www.springframework.org/schema/beans 
     http://www.springframework.org/schema/beans/spring-beans.xsd  
     http://www.springframework.org/schema/context  
     http://www.springframework.org/schema/context/spring-context.xsd"> 
<context:annotation-config/> 

<context:annotation-config/> 使容器考虑 bean 上的注解。

instead of the above configuration one can even go with the following configuration,

<bean calss=""or.springframework.bean.factory.annotation.
  AutowiredAnnotationBeanPostProcessor""/> 

让我们开始逐一使用注解来处理不同的场景,如下所示,

注解配置

这些注解是类级注解,它们与 Spring 容器一起注册。以下图表显示了典型注解:

我们在应用程序中使用@Component 注解。以 Ch02_Reference_DI 为基础项目,按照以下步骤操作:

  1. 创建 Ch02_Demo_Annotation 文件夹以及我们之前添加的所有 jar 文件,然后将 spring-aop.jar 也添加进去。

  2. 在 com.ch02.beans 包中创建或复制 Address.java 文件。

  3. 在 com.ch02.stereotype.annotation 包中创建或复制 Customer.java 文件。将其重命名为 Customer_Component.java

  4. 为其添加默认构造函数,因为我们要参考的代码中没有默认构造函数。代码可以如下所示:

      public Customer_Component() { 
        // TODO Auto-generated constructor stub 
        cust_id=100; 
        cust_name=""cust_name""; 
        cust_address=new Address(); 
        cust_address.setBuild_no(2); 
        cust_address.setCity_name(""Delhi""); 
      } 

  1. 使用以下方式用 @Component 注解类:
      @Component 
      public class Customer_Component {   } 

这里我们配置了一个可自动扫描的组件。默认情况下,这个组件的 id 将是首字母小写的类名。在我们的例子中是 ''customer_Component''。如果我们想要使用自定义的 id,我们可以修改配置以使用 ''myObject'' 作为 bean id,

      @Component(value=""myObject"")  

在 XML 中配置它是不必要的,正如我们之前所做的那样。但是我们仍然需要 XML 来进行一些其他配置。

  1. 在类路径中创建或复制 customer.xml 文件。

  2. 添加以下代码以考虑注解:

      <context:annotation-config/> 

我们已经看到了如何配置“context”命名空间。

  1. 创建 TestCustomer_Component 组件以获取 Customer_Component 组件的 bean,如下所示,以找出我们配置的输出是什么:
      public class TestCustomer_component { 
        public static void main(String[] args) { 
          // TODO Auto-generated method stub 
          ApplicationContext context = new  
            ClassPathXmlApplicationContext(""customer.xml""); 
          Customer_Component customer = (Customer_Component)  
            context.getBean(""customer_Component""); 
          System.out.println(customer.getCust_name() + ""\t"" +  
          customer.getCust_id() +  
          customer.getCust_address()); 
        } 
      } 

执行后,我们将得到以下异常堆栈跟踪,

      Exception in thread "main"  
      org.springframework.beans.factory.NoSuchBeanDefinitionException:
      No bean named ''customer_component'' is defined 

  1. 如果我们已经做了所有的事情,为什么我们仍然得到异常。原因是使组件自动扫描可用,但忘记告诉容器在哪里扫描注解?让我们配置在哪里扫描组件:
      <context:component-scan  
        base-package=""com.ch02.stereotype.*""> 
      </context:component-scan> 

在这里,将扫描 com.ch02.streotype 的所有子包,以查找具有 ''customer_Component'' id 的 bean。

  1. 添加配置后,我们将得到以下输出,显示数据成员值:
      INFO: Loading XML bean definitions from class path resource 
      [customer.xml] 
      cust_name 100 Delhi 2

同样,我们可以使用其他注解,但我们将逐一在接下来的章节中讨论它们。

注解配置

我们已经深入讨论了 ''auto wiring'',它为我们提供了减少配置和自动发现要注入哪个对象作为 bean 依赖项的便利。 以下是帮助自动注入和解决自动注入问题的注解:

让我们使用 Cho2_Demo_Annotation 项目来演示上述注解。

Case1. 使用 @Autowired
  1. 在 com.ch02.autowiring.annotation 中创建 Customer_Autowired 类。这与我们之前在 stereotype annotations 中创建的 Customer_Component 类似。如果你没有默认客户,不要忘记添加。

  2. 在如下所示的 cust_address 字段上添加 @Autowired 注解:

      @Autowired 
      private Address cust_address; 

  1. 在 customer_new.xml 中如下配置 Address bean:
      <bean id=""cust_address"" class=""com.ch02.beans.Address""> 
        <property name=""build_no"" value=""2"" /> 
        <property name=""city_name"" value=""Pune"" /> 
        <property name=""pin_code"" value=""123"" /> 
      </bean> 

  1. 创建 TestCustomer_Autowired 以获取 id 为 ''customer_Autowired'' 的 bean:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context = new  
          ClassPathXmlApplicationContext(""customer_new.xml""); 
        Customer_Autowired customer = (Customer_Autowired)  
          context.getBean(""customer_Autowired""); 
        System.out.println(customer.getCust_name() + ""\t"" +  
          customer.getCust_id() +  
          customer.getCust_address()); 
      } 

  1. 在执行时,我们将得到以下控制台输出:
      INFO: Loading XML bean definitions from class path resource
      [customer_new.xml] 
      my name  10  Mumbai  12 

Case2. 使用 autowired=false

无论开发者多么小心,我们总是遇到依赖项不可用或由于某种原因而没有注入的情况。 这里出现异常是很明显的。 为了摆脱这种强制注入,请将 'autowired=false' 添加到 @Autowired

  1. 你可以通过删除 customer.xml 中的 cust_address 来尝试这个,运行主程序我们将得到异常:
      Caused by:
      org.springframework.beans.factory.NoSuchBeanDefinitionException:
      No qualifying bean of type [com.ch02.beans.Address] found for
      dependency [com.ch02.beans.Address]: expected at least 1 bean
      which qualifies as autowire candidate for this dependency.
      Dependency annotations:
        {@org.springframework.beans.factory.annotation.Autowired 
      (required=true)} 

  1. 将 @Autowired 注解更改为:
      @Autowired(required=false) 
      private Address cust_address; 

  1. 再次重新运行主要功能,我们得到以下输出:
my name 10null

null 值表示没有注入地址,但我们这里没有 bean 创建异常。

注意

一旦你完成了 required=false 的演示,请撤销我们在这个演示中刚刚所做的所有更改。

Case3. 使用 @Qualifier

在某些情况下,我们可能容器中配置了同一类型的多个 bean。 有一个以上的 bean 不是一个问题,除非开发者控制依赖注入。 也可能有不同 id 的依赖项,以及同一类型的多个 bean,但没有匹配的 id。 让我们使用 @Qualifier 来解决这些问题。 我们将使用上一步骤中的相同类 Customer_Autowired

  1. 将之前创建的 bean cust_address 的 id 重命名为 cust_address1。

  2. 像下面这样添加一个 Address 类型的另一个 bean:

      <bean id=""address"" class=""com.ch02.beans.Address""> 
        <property name=""build_no"" value=""2"" /> 
        <property name=""city_name"" value=""Pune"" /> 
        <property name=""pin_code"" value=""123"" /> 
      </bean> 

  1. 创建 TestCustomer_Autowiring1 以获取以下所示的客户实例:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context = new  
          ClassPathXmlApplicationContext(""customer_new.xml""); 
        Customer_Autowired customer = (Customer_Autowired)  
          context.getBean(""customer_Autowired""); 
        System.out.println(customer.getCust_name() + ""\t"" +  
          customer.getCust_id() +  
          customer.getCust_address()); 
      }   

  1. 在执行时,我们将得到以下异常,指定存在多个可注入的 Address 实例,导致不明确性:
      Caused by: 
      org.springframework.beans.factory.NoUniqueBeanDefinitionException
      :No qualifying bean of type [com.ch02.beans.Address] is defined:
      expected single matching bean but found 2: cust_address1,address 

  1. 更新数据成员 cust_address 的注解为 @Qualifier,如下所示:
       @Qualifier(value=""address"") 
       private Address cust_address; 

我们指定要注入以满足依赖关系的 bean 的 id。 在我们的案例中,id 为 ''address'' 的 bean 将被注入。

  1. 执行我们在第 2 步创建的主要功能,以获得以下输出,显示 id 为 'address' 的 bean 注入到 Customer_Autowried 中:
      INFO: Loading XML bean definitions from class path resource
        [customer_new.xml] 
      my name  10  Pune  2 

Case 3. 使用 @Required

在开发代码时,如果未提供正确值或缺少这些值,业务逻辑将失败。因此,必须注入依赖项,无论如何都不能失败。为确保已注入依赖项并准备好使用,请在@Autowired注解之后应用@Required。它的工作方式与'autowired=true'相同。但与它相比,注解只能应用于 setter 方法。如果依赖项不可用,将抛出BeanInitializationException

以下代码清楚地说明了@Required的使用:

@Component(value=""cust_required"") 
public class Customer_Annot_Required { 

  private String cust_name; 
  private int cust_id; 

  private Address cust_address; 

  public void setCust_name(String cust_name) { 
    this.cust_name = cust_name; 
  } 

  public void setCust_id(int cust_id) { 
    this.cust_id = cust_id; 
  } 

  @Autowired 
  @Required 
  public void setCust_address(Address cust_address) { 
    this.cust_address = cust_address; 
  } 

  public Customer_Annot_Required() { 
    // TODO Auto-generated constructor stub 
    cust_id=10; 
    cust_name=""my name""; 
  } 
// getter methods 
} 

案例 4:作用域管理注解

几页前我们深入讨论了作用域及其使用。我们还看到了如何在 XML 中根据需求管理作用域。通常@Scope是类级注解。但它也可以与 Bean 注解一起使用,指示返回实例的作用域。

以下代码表示使用@Scope:

package com.ch02.scope.annotation; 

@Component 
@Scope(scopeName=""prototype"") 
public class Customer_Scope { 

  private String cust_name; 
  private int cust_id; 

  @Autowired   
  @Qualifier(value=""address"") 
  private Address cust_address; 

  public Customer_Scope() { 
    // TODO Auto-generated constructor stub 
    cust_id=10; 
    cust_name=""my name""; 
  } 
//getters and setters 
} 

您可以使用以下代码来检查每次是否提供了新的实例:

public class TestCustomer_Scope { 
  public static void main(String[] args) { 
    // TODO Auto-generated method stub 
    ApplicationContext context = new  
      ClassPathXmlApplicationContext(""customer_new.xml""); 
    Customer_Scope customer = (Customer_Scope)  
      context.getBean(""customer_Scope""); 
    System.out.println(customer.getCust_name() + ""\t"" +  
      customer.getCust_id() +    
      customer.getCust_address()); 
    customer.setCust_name(""new name set""); 
    Customer_Scope customer1 = (Customer_Scope)  
      context.getBean(""customer_Scope""); 
    System.out.println(customer1.getCust_name() + ""\t"" +  
      customer1.getCust_id() +  
      customer1.getCust_address()); 
    System.out.println(""after changing name and using prototype  
      scope""); 
    System.out.println(customer.getCust_name() + ""\t"" +  
      customer.getCust_id() +  
      customer.getCust_address()); 
    System.out.println(customer1.getCust_name() + ""\t""     
      + customer1.getCust_id()+   
      customer1.getCust_address()); 
  } 
} 

您可以参考 Ch02_Demo_Annotation 中的完整代码。

JSR 标准注解

Spring 2.5 支持 JSR-250,而从 3.0 开始,框架开始支持 JSR-330 标准注解,其发现方式与基于 Spring 的注解相同。JSR 提供了用于生命周期管理、bean 配置、自动注入和许多其他需求的注解。让我们逐一讨论它们。

生命周期注解

我们已经足够讨论了 bean 的生命周期以及通过 XML 配置实现它的不同方式。但我们还没有讨论基于注解的。从 Spring 2.5 开始,支持通过@PostConstruct@PreDestroy进行生命周期管理,分别提供了InitializingBean和 Disposable 接口的替代方案。

@PostConstruct:

被@PostConstruct 注解标注的方法将在容器使用默认构造函数实例化 bean 之后调用。这个方法在实例返回之前调用。

@PreDestroyed:

被@PreDestroy 注解标注的方法将在 bean 被销毁之前调用,这允许从可能因对象销毁而丢失的资源中恢复值。

让我们按照以下步骤使用它来了解 bean 的生命周期:

  1. 在我们已经在使用讨论 bean 生命周期的 Ch02_Bean_Life_Cycle java 项目中添加 Car_JSR 类,位于 com.ch02.beans 包中。

  2. 在 Car 类中添加一个 init_car 方法用于初始化汽车,并用@PostConstruct 注解标注,如下所示:

      @PostConstruct 
      public void init_car() 
      { 
         System.out.println(""initializing the car""); 
         price=(long)(price+(price*0.10)); 
      } 

  1. 在类中添加一个方法,该方法包含汽车销毁的代码,这实际上是资源释放,如下所示:
       @PreDestroy 
       public void destroy_car() 
       {   
         System.out.println(""demolishing the car""); 
       } 

  1. 添加 beans_new.xml 以配置 bean,不要忘记在应用程序中使用注解的规则。XML 将如下所示:
      <context:annotation-config/> 
        <bean id=""car"" class=""com.ch02.beans.Car""       
          scope=""prototype""> 
          <property name=""chesis_number"" value=""eng2012"" /> 
          <property name=""color"" value=""baker''s chocolate"" /> 
          <property name=""fuel_type"" value=""petrol"" /> 
          <property name=""average"" value=""12.50"" /> 
          <property name=""price"" value=""643800"" /> 
        </bean> 

  1. 在 TestCar 中添加一个 main 方法,如下所示:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context=new           
          ClassPathXmlApplicationContext(""beans.xml""); 
        // first request to get the car instance 
        Car_JSR car_one=(Car_JSR)context.getBean(""car""); 
        car_one.show(); 
      } 

  1. 执行代码后,我们将得到以下输出,显示调用了 init_car():
      INFO: Loading XML bean definitions from class path resource
        [beans.xml] 
      initializing the car 
      showing car eng2012 having color:-baker''s chocolate and 
      price:-708180 

  1. 由于销毁方法将在上下文切换后执行,所以控制台上将不会显示输出。我们可以使用以下代码优雅地关闭容器:
      ((AbstractApplicationContext)context).registerShutdownHook(); 

@Named

@Named注解用于代替应用于类级别的@Component@Named注解不提供组合模型,但其扫描方式与@Component相同:

@Named 
public class Customer { 

  private String cust_name; 
  private int cust_id; 

  private Address cust_address; 
   //default and parameterize constructor 
   //getters and setters 
} 

@Inject

@Inject用于自动注入依赖项,就像我们为@Autowired做的那样。但它不会有required属性来指定依赖是可选的。以下代码显示了实现方式:

@Named 
public class Customer { 

  private String cust_name; 
  private int cust_id; 

   @Inject 
  private Address cust_address; 
   // default and parameterized constructor 
   // getters and setters 
} 

@Configuration

类级别的@Configuration注解用于指示类作为 bean 定义的来源,就像我们在<beans></beans>标签中的 XML 文件中做的那样。

@Configuration 
public class MyConfiguration { 

// bean configuration will come here 

} 

Spring 注解如@Component@Repository需要注册到框架中。在 XML 中,通过提供一个<context:component-scan>属性 base-package 来启用这些注解的扫描。@Component-Scan是启用 Spring stereotypes 注解扫描的替代方案。语法如下所示:

@Component-Scan(basePackage=name_of_base_package) 

@Bean

@Bean@Configuration一起使用,用来声明 bean 定义,就像我们通常在 XML 中的<bean></bean>标签中做的那样。它适用于有 bean 定义的方法。代码可以如下所示:

@Configuration 
public class MyConfiguration { 
    @Bean 
    public Customer myCustomer() 
    { 
        Customer customer=new Customer(); 
        customer.setCust_name(""name by config""); 
      return customer; 
   } 
} 

要定义从myCustomer()返回的 bean,也可以有自定义的 bean id,可以通过:

@Bean(name=""myCustomer"") 

注解配置已经取代了 XML。AnnotationConfigApplicationContext类帮助以与ClasspathXmlApplicationContext相同的方式加载配置。测试类可以写成:

public static void main(String[] args) { 
  ApplicationContext context=new   
     AnnotationConfigApplicationContext(MyConfiguration.class); 
  Customer customer=(Customer)context.getBean(""myCustomer""); 
  System.out.println(customer.getCust_id()+""\t""+ 
    customer.getCust_name()); 
} 

你可以参考 Ch02_Demo_JSR_Annot 中的完整代码。

注意

XML 提供了一种集中进行 bean 配置的方式。由于依赖项被保持在源代码之外,它们的更改不会影响源代码。此外,在 XML 配置中,源代码不需要重新编译。但是,基于注解的配置直接是源代码的一部分,并且散布在整个应用程序中。这变得去中心化,最终变得难以控制。基于注解注入的值将被 XML 注入覆盖,因为基于注解的注入发生在 XML 注入之前。

Spring 表达式语言(SpEL)

到目前为止,我们配置了次要类型的值。对于 bean 的连接,我们使用了我们在编译时编写并可用的 XML 配置。但是使用它我们无法配置运行时值。Spring 表达式提供了所需的解决方案,其值将在运行时进行评估。

开发人员可以使用 SpEL 引用其他 bean 并设置字面量值。它提供了一种调用对象的方法和属性的手段。SpEL 可以评估使用数学、关系和逻辑运算符的值。它还支持集合的注入。这与在 JSP 中使用 EL 有些相似。使用 SpEL 的语法是'#{value}'。让我们逐一找出如何在应用程序中使用 SpEL。

使用字面量

在 SpEL 中使用字面量允许开发人员为 bean 的属性设置原始值。虽然在配置中使用字面量并不有趣,但了解如何在表达式中使用确实帮助我们走向复杂的表达式。按照以下步骤了解字面量的使用,

  1. 将 Ch02_SpringEL 创建为 Java 应用程序,并添加 Spring 库。

  2. 在 com.ch02.beans 中定义 Customer.java,如下所示:

      public class Customer { 
        private String cust_name; 
        private String cust_id; 
        private boolean second_handed; 
        private double prod_price; 
        private String prod_name; 
        // getters and setters 
        // toString() 
      } 

  1. 在类路径中创建 beans.xml 文件,以如下方式配置客户 bean:
      <bean id=""cust_new"" class=""com.ch02.beans.Customer""> 
        <property name=""cust_name"" value=""Bina"" /> 
        <property name=""cust_id"" value=""#{2}"" /> 
        <property name=""prod_name"" value=""#{''Samsung Fridge''}"" /> 
        <property name=""prod_price"" value=""#{27670.50}"" /> 
        <property name=""second_handed"" value=""#{false}"" /> 
      </bean> 

你可以看到 cust_id、prod_price 使用 SpEL 语法配置为'#{ value}',并为 prod_name 使用单引号指定值。你甚至可以使用双引号指定字符串值。cust_name 已使用旧风格配置。是的,仍然可以同时使用旧风格和 SpEL 设置值。

  1. 按照下面的代码添加 TestCustomer.java:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
          ApplicationContext context=new     
          ClassPathXmlApplicationContext(""beans.xml""); 
        Customer customer=(Customer)context.getBean(""cust_new""); 
        System.out.println(customer); 
      } 

  1. 我们将得到如下输出:
      INFO: Loading XML bean definitions from class path resource 
      [beans.xml] 
      Bina has bought Samsung Fridge for Rs 27670.5 

使用 bean 引用

在 XML 中配置的每个 bean 都有其唯一的 bean id。这可以用于引用值或使用 SpEL 进行自动装配。按照以下步骤了解如何使用 bean 引用。

  1. 在上面的 Ch02_SpringEL 项目中添加新的 POJO 类 Address 和 Customer_Reference,如下所示:
      public class Address { 
        private String city_name; 
        private int build_no; 
        private long pin_code; 

        // getter and setter 
        @Override 
        public String toString() { 
          // TODO Auto-generated method stub 
          return city_name+ "",""+pin_code; 
        } 
      } 
      public class Customer_Reference { 
        private String cust_name; 
        private int cust_id; 
        private Address cust_address; 

        // getter and sertter 

        @Override 
        public String toString() { 
          return cust_name + "" is living at "" + cust_address; 
        } 
      } 

  1. 在 beans.xml 中添加 Address 和 Customer_Reference bean,如下所示:
      <bean id=""cust_address"" class=""com.ch02.beans.Address""> 
        <property name=""build_no"" value=""2"" /> 
        <property name=""city_name"" value=""Pune"" /> 
        <property name=""pin_code"" value=""123"" /> 
      </bean> 
      <bean id="cust_ref" class=""com.ch02.beans.Customer_Reference""> 
        <property name=""cust_name"" value=""Bina"" /> 
        <property name=""cust_id"" value=""#{2}"" /> 
        <property name=""cust_address"" value=""#{cust_address}"" /> 
      </bean> 

注意 cust_address 的初始化方式。将客户地址赋值给 cust_address,这是为 Address 定义的 bean 的 id。

  1. 按照第 4 步的方法定义 TestCustomer_Reference,以获取'cust_ref'bean。

  2. 执行后,我们将得到如下输出:

      INFO: Loading XML bean definitions from class path resource
      [beans.xml] 
      Bina is living at Pune,123 

  1. 有时,我们可能希望注入 bean 的某个属性,而不是使用整个 bean,如下所示:
      <bean id="cust_ref_new"class="com.ch02.beans.Customer_Reference"> 
        <property name=""cust_name""  
          value=""#{cust_ref.cust_name.toUpperCase()}"" /> 
        <property name=""cust_id"" value=""#{2}"" /> 
        <property name=""cust_address"" value=""#{cust_address}"" /> 
      </bean> 

我们通过从 bean 'cust_ref'借用 cust_name 并将其转换为大写来注入它。

使用数学、逻辑、关系运算符

在某些场景中,依赖项的值需要使用数学、逻辑或关系运算符来定义和放置。你可以找到如何使用它们的演示。

  1. 我们将使用前面案例中定义的同一个项目 Ch02_SpringEL。

  2. 在 beans.xml 中定义 Customer bean,如下所示:

      <bean id=""cust_calculation"" class="com.ch02.beans.Customer""> 
        <property name="cust_name" value="Bina" /> 
        <property name="cust_id" value="#{2}" /> 
        <property name="prod_name" value="#{''Samsung Fridge''}" /> 
        <property name="prod_price" value="#{27670.50*12.5}" /> 
        <property name="second_handed"  
          value="#{cust_calculation.prod_price > 25000}" /> 
      </bean> 

prod_price 的值通过数学运算符在运行时计算,并通过关系运算符评估'second_handed'的值。

  1. 编写 TestCustomer_Cal 以获取 cust_calculation 并如下显示其属性:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context=new  
          ClassPathXmlApplicationContext(""beans.xml""); 
        Customer customer=    
          (Customer)context.getBean(""cust_calculation""); 
        System.out.println(customer); 
        System.out.println(customer.isSecond_handed()); 
      } 

在整个章节中,我们都看到了如何进行配置。但我们忽略了一个非常重要的点——“松耦合”。我们使用的类导致了紧密耦合,而我们知道,通过合同编程可以给开发者提供编写松耦合模块的便利。我们避免在上面的所有示例中使用接口,因为它可能会使它们变得不必要复杂。但在实际的应用程序中,您会在 Spring 的每个阶段发现接口的使用。从下一章节开始,我们将使用编写 Spring 应用程序的标准方式。

总结

(此处原文为三个星号,表示空白行)

本章充满了各种配置,不同的方法和替代方案,它们都可以用来完成同一件事情。无论您使用的是 Spring 的 5.0 版本还是更低版本,本章都会在您的学习旅程中提供帮助。在本章中,我们探讨了实例创建和初始化的各种方法。现在,我们已经了解了几乎所有的 Java 概念以及如何使用 XML 和基于注解的方式来配置它们。我们还看到了诸如自动注入、SpEL 等减少配置的方法。我们展示了 Spring 提供的以及 JSR 注解。

现在我们已经准备好将这些内容应用到我们的应用程序中。让我们从数据库处理开始。在下一章节中,我们将了解如何使用 Spring 来处理数据库,以及 Spring 如何帮助我们进行更快的开发。

第三章。使用 Spring DAO 加速

在第二章节中,我们深入讨论了依赖注入。显然,我们讨论了在配置文件中以及使用注解的各种使用 DI 的方法,但由于缺乏实时应用程序,这些讨论仍然不完整。我们没有其他选择,因为这些都是每个 Spring 框架开发者都应该了解的最重要的基础知识。现在,我们将开始处理数据库,这是应用程序的核心。

我们将讨论以下配置:

  • 我们将讨论关于 DataSource 及其使用 JNDI、池化 DataSource 和 JDBCDriver based DataSource 的配置。

  • 我们将学习如何使用 DataSource 和 JDBCTemplate 将数据库集成到应用程序中。

  • 接下来我们将讨论如何使用 SessionFactory 在应用程序中理解 ORM 及其配置。

  • 我们将配置 HibernateTemplate 以使用 ORM 与数据库通信。

  • 我们将讨论如何配置缓存管理器以支持缓存数据。

我们都知道,数据库为数据提供了易于结构化的方式,从而可以使用各种方法轻松访问。不仅有许多可用的方法,市场上还有许多不同的数据库。一方面,拥有多种数据库选项是好事,但另一方面,因为每个数据库都需要单独处理,所以这也使得事情变得复杂。在 Java 应用程序的庞大阶段,需要持久性来访问、更新、删除和添加数据库中的记录。JDBC API 通过驱动程序帮助访问这些记录。JDBC 提供了诸如定义、打开和关闭连接,创建语句,通过 ResultSet 迭代获取数据,处理异常等低级别的数据库操作。但是,到了某个点,这些操作变得重复且紧密耦合。Spring 框架通过 DAO 设计模式提供了一种松耦合、高级、干净的解决方案,并有一系列自定义异常。

Spring 如何处理数据库?


在 Java 应用程序中,开发者通常使用一个工具类概念来创建、打开和关闭数据库连接。这是一种非常可靠、智能且可重用的连接管理方式,但应用程序仍然与工具类紧密耦合。如果数据库或其连接性(如 URL、用户名、密码或模式)有任何更改,需要在类中进行更改。这需要重新编译和部署代码。在这种情况下,外部化连接参数将是一个好的解决方案。我们无法外部化 Connection 对象,这仍然需要开发者来管理,同样处理它时出现的异常也是如此。Spring 有一种优雅的方式来管理连接,使用位于中心的 DataSource。

DataSource

数据源是数据源连接的工厂,类似于 JDBC 中的 DriverManager,它有助于连接管理。以下是一些可以在应用程序中使用的实现,以获取连接对象:

  • DriverManagerDataSource:该类提供了一个简单的 DataSource 实现,用于在测试或独立应用程序中,每次请求通过 getConnection()获取一个新的 Connection 对象。

  • SingleConnectionDataSource:这个类是 SmartDatSource 的一个实现,它提供了一个不会在使用后关闭的单一 Connection 对象。它只适用于单线程应用程序,以及在应用程序服务器之外简化测试。

  • DataSourceUtils:这是一个辅助类,它有静态方法用于从 DataSource 获取数据库连接。

  • DataSourceTransactionManager:这个类是 PlatformTransactionManager 的一个实现,用于每个数据源的连接。

配置数据源

数据源可以通过以下方式在应用程序中配置:

  • 从 JNDI 查找获取:Spring 有助于维护在应用程序服务器(如 Glassfish、JBoss、Wlblogic、Tomcat)中运行的大型 Java EE 应用程序。所有这些服务器都支持通过 Java 命名目录接口(JNDI)查找配置数据源池的功能,这有助于提高性能。可以使用以下方式获取在应用程序中配置的数据源:
      <beans xmlns="http://www.springframework.org/schema/beans" 
        xmlns:jee="http://www.springframework.org/schema/jee" 
        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 http://www.springframework.org/schema/jee
        http://www.springframework.org/schema/jee/spring-jee.xsd"> 

        <bean id="dataSource"  
          class="org.springframework.jndi.JndiObjectFactoryBean"> 
          <property name="jndiName"   
            value="java:comp/env/jdbc/myDataSource"/> 
        </bean> 

        <jee:jndi-lookup jndi-name="jdbc/myDataSource" id="dataSource" 
          resource-ref="true"/> 
      </beans> 

  • 其中:

  • jndi-name:指定在 JNDI 中配置的服务器上的资源名称。

  • id:bean 的 id,提供 DataSource 对象。

  • resource-ref:指定是否需要前缀 java:comp/env。

  • 从池中获取连接:Spring 没有池化数据源,但是,我们可以配置由 Jakarta Commons Database Connection Pooling 提供的池化数据源。DBCP 提供的 BasicDataSource 可以配置为,

      <bean id="dataSource"               
         class="org.apache.commons.dbcp.BasicDataSource">        
        <property name="driverClassName"                 
          value="org.hsqldb.jdbcDriver"/> 
        <property name="url"    
            value="jdbc:hsqldb:hsql://locahost/name_of_schama"/> 
        <property name="username"      
            value="credential_for_username"/> 
        <property name="password"      
            value="credential_for_password"/> 
        <property name="initialSize"      
            value=""/> 
        <property name="maxActive"      
            value=""/> 
      </bean> 

  • 其中:

  • initialSize:指定了当池启动时应创建的连接数量。

  • maxActive:指定可以从池中分配的连接数量。

  • 除了这些属性,我们还可以指定等待连接从池中返回的时间(maxWait),连接可以在池中空闲的最大/最小数量(maxIdle/minIdle),可以从语句池分配的最大预处理语句数量(maxOperationPreparedStatements)。

  • 使用 JDBC 驱动器:可以利用以下类以最简单的方式获取 DataSource 对象:

  • SingleConnectionDataSource:正如我们已经在讨论中提到的,它返回一个连接。

  • DriverManagerDataSource:它在一个请求中返回一个新的连接对象。

  • 可以按照以下方式配置 DriverMangerDataSource:
      <bean id="dataSource"
        class="org.springframework.jdbc.datasource.
        DriverManagerDataSource">        
        <property name="driverClassName"                 
          value="org.hsqldb.jdbcDriver"/> 
        <property name="url"    
          value="jdbc:hsqldb:hsql://locahost/name_of_schama"/> 
        <property name="username"      
          value="credential_for_username"/> 
        <property name="password"      
          value="credential_for_password"/> 
      </bean> 

注意

单连接数据源适用于小型单线程应用程序。驱动管理数据源支持多线程应用程序,但由于管理多个连接,会损害性能。建议使用池化数据源以获得更好的性能。

让我们开发一个使用松耦合模块的示例 demo,以便我们了解 Spring 框架应用程序开发的实际方面。

数据源有助于处理与数据库的连接,因此需要在模块中注入。使用 setter DI 或构造函数 DI 的选择完全由您决定,因为您很清楚这两种依赖注入。我们将使用 setter DI。我们从考虑接口开始,因为这是根据合同进行编程的最佳方式。接口可以有多个实现。因此,使用接口和 Spring 配置有助于实现松耦合模块。我们将声明数据库操作的方法,您可以选择签名。但请确保它们可以被测试。由于松耦合是框架的主要特性,应用程序也将演示为什么我们一直在说松耦合模块可以使用 Spring 轻松编写?您可以使用任何数据库,但本书将使用 MySQL。无论您选择哪种数据库,都要确保在继续之前安装它。让我们按照步骤开始!

案例 1:使用 DriverManagerDataSource 的 XML 配置
  1. 创建一个名为 Ch03_DataSourceConfiguration 的核心 Java 应用程序,并添加 Spring 和 JDBC 的 jar,如下所示:

  1. 在 com.ch03.packt.beans 包中创建一个 Book POJO,如下所示:
      public class Book { 
        private String bookName; 
        private long ISBN; 
        private String publication; 
        private int price; 
        private String description; 
        private String [] authors; 

        public Book() { 
          // TODO Auto-generated constructor stub 
          this.bookName="Book Name"; 
          this.ISBN =98564567l; 
          this.publication="Packt Publication"; 
          this.price=200; 
          this.description="this is book in general"; 
          this.author="ABCD"; 
        } 

        public Book(String bookName, long ISBN, String  
          publication,int price,String description,String  
          author)  
       { 
          this.bookName=bookName; 
          this.ISBN =ISBN; 
          this.publication=publication; 
          this.price=price; 
          this.description=description; 
           this.author=author; 
        } 
        // getters and setters 
        @Override 
        public String toString() { 
          // TODO Auto-generated method stub 
          return bookName+"\t"+description+"\t"+price; 
        } 
      }
  1. 在 com.ch03.packt.dao 包中声明一个 BookDAO 接口。(DAO 表示数据访问对象)。

  2. 在数据库中添加书籍的方法如下所示:

      interface BookDAO 
      { 
        public int addBook(Book book); 
      } 

  1. 为 BookDAO 创建一个实现类 BookDAOImpl,并在类中添加一个 DataSource 类型的数据成员,如下所示:
      private DataSource dataSource; 

  • 不要忘记使用标准的 bean 命名约定。
  1. 由于我们采用 setter 注入,请为 DataSource 编写或生成 setter 方法。

  2. 覆盖的方法将处理从 DataSource 获取连接,并使用 PreaparedStatement 将 book 对象插入表中,如下所示:

      public class BookDAOImpl implements BookDAO { 
        private DataSource dataSource; 

        public void setDataSource(DataSource dataSource) { 
          this.dataSource = dataSource; 
        } 

      @Override
      public int addBook(Book book) { 
          // TODO Auto-generated method stub 
          int rows=0; 
          String INSERT_BOOK="insert into book values(?,?,?,?,?,?)"; 
          try { 
            Connection connection=dataSource.getConnection(); 
            PreparedStatement ps=  
                   connection.prepareStatement(INSERT_BOOK); 
            ps.setString(1,book.getBookName()); 
            ps.setLong(2,book.getISBN()); 
            ps.setString(3,book.getPublication()); 
            ps.setInt(4,book.getPrice()); 
            ps.setString(5,book.getDescription()); 
            ps.setString(6,book.getAuthor()); 
            rows=ps.executeUpdate(); 
          } catch (SQLException e) { 
            // TODO Auto-generated catch block 
            e.printStackTrace(); 
          } 
          return rows; 
        } 
      } 

  1. 在类路径中创建 connection.xml 以配置 beans。

  2. 现在问题变成了需要声明多少个 bean 以及它们各自的 id 是什么?

注意

一个非常简单的经验法则:首先找出要配置的类,然后找出它的依赖关系是什么?

以下是:

一个 BookDAOImpl 的 bean。

BookDAOImpl 依赖于 DataSource,因此需要一个 DataSource 的 bean。

您可能会惊讶 DataSource 是一个接口!那么我们是如何创建并注入其对象的呢?是的,这就是我们观点所在!这就是我们所说的松耦合模块。在这里使用的 DataSource 实现是 DriverManagerDataSource。但如果我们直接注入 DriverManagerDataSource,那么类将与它紧密耦合。此外,如果明天团队决定使用其他实现而不是 DriverManagerDataSource,那么代码必须更改,这导致重新编译和重新部署。这意味着更好的解决方案将是使用接口,并从配置中注入其实现。

id 可以是开发者的选择,但不要忽略利用自动装配的优势,然后相应地设置 id。这里我们将使用自动装配'byName',所以请选择相应的 id。(如果您困惑或想深入了解自动装配,可以参考上一章。)因此,XML 中的最终配置将如下所示:

<bean id="dataSource" 
  class= 
   "org.springframework.jdbc.datasource.DriverManagerDataSource"> 
    <property name="driverClassName"    
        value="com.mysql.jdbc.Driver" /> 
    <property name="url"  
        value="jdbc:mysql://localhost:3306/bookDB" /> 
    <property name="username" value="root" /> 
    <property name="password" value="mysql" /> 
  </bean> 

  <bean id="bookDao" class="com.packt.ch03.dao.BookDAOImpl" 
     autowire="byname"> 
  </bean> 

您可能需要根据您的连接参数自定义 URL、用户名和密码。

  1. 通常,DAO 层将由服务层调用,但在这里我们不处理它,因为随着应用程序的进行,我们将添加它。由于我们还没有讨论测试,我们将编写带有 main 函数的代码来找出它的输出。main 函数将获取 BookDAO bean 并在其上调用插入 Book 的方法。如果实现代码返回的行值大于零,则书籍成功添加,否则不添加。创建一个名为 MainBookDAO 的类,并向其添加以下代码:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context=new  
          ClassPathXmlApplicationContext("connection.xml"); 
        BookDAO bookDAO=(BookDAO) context.getBean("bookDao"); 
        int rows=bookDAO.addBook(new Book("Learning Modular     
          Java Programming", 9781234,"PacktPub   
          publication",800,"explore the power of   
          Modular programming","T.M.Jog"));        
        if(rows>0) 
        { 
          System.out.println("book inserted successfully"); 
        } 
        else 
          System.out.println("SORRY!cannot add book"); 
      }  

如果你仔细观察我们会配置 BookDAOImpl 对象,我们是在 BookDAO 接口中接受它,这有助于编写灵活的代码,在这种代码中,主代码实际上不知道是哪个对象在提供实现。

  1. 打开你的 MYSQL 控制台,使用凭据登录。运行以下查询创建 BookDB 架构和 Book 表:

  1. 一切都准备好了,执行代码以在控制台获得“book inserted successfully”(书籍插入成功)的提示。你也可以在 MySQL 中运行“select * from book”来获取书籍详情。
Case2:使用注解 DriverManagerDataSource

我们将使用在 Case1 中开发的同一个 Java 应用程序 Ch03_DataSourceConfiguration:

  1. 声明一个类 BookDAO_Annotation,在 com.packt.ch03.dao 包中实现 BookDAO,并用@Repository 注解它,因为它处理数据库,并指定 id 为'bookDAO_new'。

  2. 声明一个类型为 DataSource 的数据成员,并用@Autowired 注解它以支持自动装配。

  • 不要忘记使用标准的 bean 命名约定。

  • 被覆盖的方法将处理数据库将书籍插入表中。代码将如下所示:

      @Repository(value="bookDAO_new") 
      public class BookDAO_Annotation implements BookDAO { 
        @Autowired 
        private DataSource dataSource; 

        @Override 
        public int addBook(Book book) { 
          // TODO Auto-generated method stub 
          int rows=0; 
          // code similar to insertion of book as shown in     
          Case1\. 
          return rows; 
        } 
      } 

  1. 我们可以编辑 Case1 中的同一个 connection.xml,但这可能会使配置复杂。所以,让我们在类路径中创建 connection_new.xml,以配置容器考虑注解并搜索如下立体注解:
      <context:annotation-config/> 
 <context:component-scan base- 
        package="com.packt.ch03.*"></context:component-scan> 

      <bean id="dataSource" 
        class="org.springframework.jdbc.datasource.
        DriverManagerDataSo urce"> 
        <!-add properties similar to Case1 -- > 
      </bean> 

  • 要找出如何添加上下文命名空间和使用注解,请参考第二章。
  1. 是时候通过以下代码找到输出:
      public class MainBookDAO_Annotation { 
          public static void main(String[] args) { 
          // TODO Auto-generated method stub 
          ApplicationContext context=new  
            ClassPathXmlApplicationContext("connection_new.xml"); 

          BookDAO bookDAO=(BookDAO) context.getBean("bookDAO_new"); 
          int rows=bookDAO.addBook(new Book("Learning Modular Java  
             Programming", 9781235L,"PacktPub  
             publication",800,"explore the power of  
             Modular programming","T.M.Jog")); 
          if(rows>0) 
          { 
            System.out.println("book inserted successfully"); 
          } 
          else 
          System.out.println("SORRY!cannot add book"); 
        } 
      } 

执行代码以将书籍添加到数据库中。

您可能已经注意到,我们从未知道是谁提供了 JDBC 代码的实现,并且由于配置,注入是松耦合的。尽管如此,我们还是能够将数据插入数据库,但仍然需要处理 JDBC 代码,例如获取连接、从它创建语句,然后为表的列设置值,以插入记录。这是一个非常初步的演示,很少在 JavaEE 应用程序中使用。更好的解决方案是使用 Spring 提供的模板类。

使用模板类执行 JDBC 操作

模板类提供了一种抽象的方式来定义操作,通过摆脱常见的打开和维护连接、获取 Statement 对象等冗余代码的问题。Spring 提供了许多这样的模板类,使得处理 JDBC、JMS 和事务管理变得比以往任何时候都容易。JdbcTemplate 是 Spring 的这样一个核心组件,它帮助处理 JDBC。要处理 JDBC,我们可以使用以下三种模板之一。

JDBCTemplate

JdbcTemplate 帮助开发者专注于应用程序的核心业务逻辑,而无需关心如何打开或管理连接。他们不必担心如果忘记释放连接会怎样?所有这些事情都将由 JdbcTemplate 为您优雅地完成。它提供了指定索引参数以在 SQL 查询中使用 JDBC 操作的方法,就像我们在 PreparedStatements 中通常做的那样。

SimpleJdbcTemplate

这与 JDBCTemplate 非常相似,同时具有 Java5 特性的优势,如泛型、可变参数、自动装箱。

NamedParameterJdbcTemplate

JdbcTemplate 使用索引来指定 SQL 中参数的值,这可能使记住参数及其索引变得复杂。如果您不习惯数字或需要设置更多参数,我们可以使用 NamedParamterJdbcTemplate,它允许使用命名参数来指定 SQL 中的参数。每个参数都将有一个以冒号(:)为前缀的命名。我们将在开发代码时看到语法。

让我们逐一演示这些模板。

使用 JdbcTemplate

我们将使用与 Ch03_DataSourceConfiguration 中相似的项目结构,并按照以下步骤重新开发它,

  1. 创建一个名为 Ch03_JdbcTemplate 的新 Java 应用程序,并添加我们在 Ch03_DataSourceIntegration 中使用的 jar 文件。同时添加 spring-tx-5.0.0.M1.jar。

  2. 在 com.packt.ch03.beans 包中创建或复制 Book。

  3. 在 com.packt.ch03.dao 包中创建或复制 BookDAO。

  4. 在 com.packt.ch03.dao 包中创建 BookDAOImpl_JdbcTemplate,并向其添加 JdbcTemplate 作为数据成员。

  5. 分别用@Repository 注解类,用@Autowired 注解数据成员 JdbcTemplate。

  6. 覆盖的方法将处理表中书籍的插入。但我们不需要获取连接。同时,我们也不会创建并设置 PreparedStatement 的参数。JdbcTemplate 会为我们完成这些工作。从下面的代码中,事情会变得相当清晰:

      @Repository (value = "bookDAO_jdbcTemplate") 
      public class BookDAO_JdbcTemplate implements BookDAO { 

        @Autowired 
        JdbcTemplate jdbcTemplate; 

        @Override 
        public int addBook(Book book) { 
          // TODO Auto-generated method stub 
          int rows = 0; 
          String INSERT_BOOK = "insert into book  
             values(?,?,?,?,?,?)"; 

          rows=jdbcTemplate.update(INSERT_BOOK, book.getBookName(),                              book.getISBN(), book.getPublication(),   
            book.getPrice(),book.getDescription(),  
            book.getAuthor()); 
          return rows; 
        } 
      } 

  • JdbcTemplate 有一个 update()方法,开发人员需要在该方法中传递 SQL 查询以及查询参数的值。因此,我们可以用它来插入、更新和删除数据。其余的都由模板完成。如果你仔细观察,我们没有处理任何异常。我们忘记了吗?不,我们不关心处理它们,因为 Spring 提供了 DataAccessException,这是一个未检查的异常。所以放心吧。在接下来的页面中,我们将讨论 Spring 提供的异常。

  • 在代码中添加一个更新书籍价格以及删除书籍的方法。不要忘记首先更改接口实现。代码如下:

      @Override 
      public int updateBook(long ISBN, int price) { 
        // TODO Auto-generated method stub 
        int rows = 0; 
        String UPDATE_BOOK = "update book set price=? where ISBN=?"; 

        rows=jdbcTemplate.update(UPDATE_BOOK, price,ISBN); 
        return rows; 
      } 

      @Override 
      public boolean deleteBook(long ISBN) { 
        // TODO Auto-generated method stub 
        int rows = 0; 
        boolean flag=false; 
        String DELETE_BOOK = "delete from book where ISBN=?"; 

        rows=jdbcTemplate.update(DELETE_BOOK, ISBN); 
        if(rows>0) 
        flag=true; 

        return flag; 
      } 

  1. 我们在 connection_new.xml 中添加一个 beans 配置文件。你可以简单地从 Ch03_DataSourceIntegration 项目中复制它。我们使用的是 JdbcTemplate,它依赖于 DataSource。因此,我们需要像下面所示配置两个 bean,一个用于 DataSource,另一个用于 JdbcTemplate:
      <context:annotation-config/> 
      <context:component-scan base- 
        package="com.packt.ch03.*"></context:component-scan> 

      <bean id="dataSource" 
        class="org.springframework.jdbc.datasource.
        DriverManagerDataSource"> 
        <property name="driverClassName"  
          value="com.mysql.jdbc.Driver" /> 
        <property name="url"  
          value="jdbc:mysql://localhost:3306/bookDB" /> 
        <property name="username" value="root" /> 
        <property name="password" value="mysql" /> 
      </bean> 

      <bean id="jdbcTemplate"  
        class="org.springframework.jdbc.core.JdbcTemplate"> 
        <property name="dataSource" ref="dataSource"></property> 
      </bean> 

  1. 编写代码以获取'bookDAO_jdbcTemplate' bean,并在 MainBookDAO_operations 中执行操作,如下所示:
      public class MainBookDAO_operations { 
        public static void main(String[] args) { 
          // TODO Auto-generated method stub 
          ApplicationContext context=new  
            ClassPathXmlApplicationContext("connection_new.xml"); 
          BookDAO bookDAO=(BookDAO)  
            context.getBean("bookDAO_jdbcTemplate"); 
          //add book 
          int rows=bookDAO.addBook(new Book("Java EE 7 Developer  
             Handbook", 97815674L,"PacktPub  
             publication",332,"explore the Java EE7  
             programming","Peter pilgrim")); 
          if(rows>0) 
          { 
            System.out.println("book inserted successfully"); 
          } 
          else 
            System.out.println("SORRY!cannot add book"); 
          //update the book 
          rows=bookDAO.updateBook(97815674L,432); 
          if(rows>0) 
          { 
            System.out.println("book updated successfully"); 
          } 
          else 
            System.out.println("SORRY!cannot update book"); 
          //delete the book 
          boolean deleted=bookDAO.deleteBook(97815674L); 
          if(deleted) 
          { 
            System.out.println("book deleted successfully"); 
          } 
          else 
            System.out.println("SORRY!cannot delete book"); 
        } 
      } 

使用 NamedParameterJdbc 模板

我们将使用 Ch03_JdbcTemplates 来添加一个新的类进行此次演示,具体步骤如下。

  1. 在 com.packt.ch03.dao 包中添加 BookDAO_NamedParameter 类,它实现了 BookDAO,并用我们之前所做的@Repository 注解。

  2. 在其中添加一个 NamedParameterJdbcTemplate 作为数据成员,并用@Autowired 注解它。

  3. 使用 update()实现覆盖方法以执行 JDBC 操作。NamedParameterJdbcTemplate 支持在 SQL 查询中给参数命名。找到以下查询以添加书籍:

      String INSERT_BOOK = "insert into book
        values(:bookName,:ISBN,:publication,:price,:description,
        : author)";

注意

每个参数都必须以前缀冒号:name_of_parameter。

  • 如果这些是参数的名称,那么这些参数需要注册,以便框架将它们放置在查询中。为此,我们必须创建一个 Map,其中这些参数名称作为键,其值由开发者指定。以下代码将给出清晰的概念:
      @Repository(value="BookDAO_named") 
      public class BookDAO_NamedParameter implements BookDAO { 

        @Autowired 
        private NamedParameterJdbcTemplate namedTemplate; 

        @Override 
        public int addBook(Book book) { 
          // TODO Auto-generated method stub 
          int rows = 0; 
          String INSERT_BOOK = "insert into book  
            values(:bookName,:ISBN,:publication,:price, 
            :description,:author)"; 
          Map<String,Object>params=new HashMap<String,Object>(); 
          params.put("bookName", book.getBookName()); 
          params.put("ISBN", book.getISBN()); 
          params.put("publication", book.getPublication()); 
          params.put("price",book.getPrice()); 
          params.put("description",book.getDescription()); 
          params.put("author", book.getAuthor()); 
          rows=namedTemplate.update(INSERT_BOOK,params);  

          return rows; 
        } 

        @Override 
        public int updateBook(long ISBN, int price) { 
          // TODO Auto-generated method stub 
          int rows = 0; 
          String UPDATE_BOOK =  
           "update book set price=:price where ISBN=:ISBN"; 

          Map<String,Object>params=new HashMap<String,Object>(); 
          params.put("ISBN", ISBN); 
          params.put("price",price); 
          rows=namedTemplate.update(UPDATE_BOOK,params); 
          return rows; 
        } 

        @Override 
        public boolean deleteBook(long ISBN) { 
          // TODO Auto-generated method stub 
          int rows = 0; 
          boolean flag=false; 
          String DELETE_BOOK = "delete from book where ISBN=:ISBN"; 

          Map<String,Object>params=new HashMap<String,Object>(); 
          params.put("ISBN", ISBN); 
          rows=namedTemplate.update(DELETE_BOOK, params); 
          if(rows>0) 
            flag=true; 
          return flag; 
        } 
      } 

  1. 在 connection_new.xml 中为 NamedParameterJdbcTemplate 添加一个 bean,如下所示:
      <bean id="namedTemplate" 
        class="org.springframework.jdbc.core.namedparam. 
          NamedParameterJdbcTemplate">   
        <constructor-arg ref="dataSource"/> 
      </bean> 

  • 在其他所有示例中我们都使用了 setter 注入,但在这里我们无法使用 setter 注入,因为该类没有默认构造函数。所以,只使用构造函数依赖注入。
  1. 使用开发的 MainBookDAO_operations.java 来测试 JdbcTemplate 的工作。你只需要更新将获取BookDAO_named bean 以执行操作的语句。更改后的代码将是:
      BookDAO bookDAO=(BookDAO) context.getBean("BookDAO_named"); 

  • 你可以在 MainBookDAO_NamedTemplate.java 中找到完整的代码。
  1. 执行代码以获取成功消息。

在小型 Java 应用程序中,代码将具有较少的 DAO 类。因此,对于每个 DAO 使用模板类来处理 JDBC,对开发人员来说不会很复杂。这也导致了代码的重复。但是,当处理具有更多类的企业应用程序时,复杂性变得难以处理。替代方案将是,不是在每个 DAO 中注入模板类,而是选择一个具有模板类能力的父类。Spring 具有 JdbcDaoSupport,NamedParameterJdbcSupport 等支持性 DAO。这些抽象支持类提供了一个公共基础,避免了代码的重复,在每个 DAO 中连接属性。

让我们继续同一个项目使用支持 DAO。我们将使用 JdbcDaoSupport 类来了解实际方面:

  1. 在 com.packt.ch03.dao 中添加 BookDAO_JdbcTemplateSupport.java,它继承了 JdbcDaoSupport 并实现了 BookDAO。

  2. 从接口覆盖方法,这些方法将处理数据库。BookDAO_JdbcTemplateSupport 类从 JdbcDaoSupport 继承了 JdbcTemplate。所以代码保持不变,就像我们使用 JdbcTemplate 时一样,稍作改动。必须通过下面的代码中加粗的 getter 方法访问 JdbcTemplate:

      @Repository(value="daoSupport") 
      public class BookDAO_JdbcTemplateSupport extends JdbcDaoSupport  
        implements BookDAO 
      { 
        @Autowired 
        public BookDAO_JdbcTemplateSupport(JdbcTemplate jdbcTemplate) 
        { 
          setJdbcTemplate(jdbcTemplate); 
        } 

        @Override 
        public int addBook(Book book) { 
          // TODO Auto-generated method stub 
          int rows = 0; 
          String INSERT_BOOK = "insert into book values(?,?,?,?,?,?)"; 

          rows=getJdbcTemplate().update(INSERT_BOOK,  
            book.getBookName(), book.getISBN(),  
            book.getPublication(), book.getPrice(), 
            book.getDescription(), book.getAuthor()); 

          return rows; 
        } 

        @Override 
        public int updateBook(long ISBN, int price) { 
          // TODO Auto-generated method stub 
          int rows = 0; 
          String UPDATE_BOOK = "update book set price=? where ISBN=?"; 

          rows=getJdbcTemplate().update(UPDATE_BOOK, price,ISBN); 
          return rows; 
        } 

        @Override 
        public boolean deleteBook(long ISBN) { 
          // TODO Auto-generated method stub 
          int rows = 0; 
          boolean flag=false; 
          String DELETE_BOOK = "delete from book where ISBN=?"; 

          rows=getJdbcTemplate().update(DELETE_BOOK, ISBN); 
          if(rows>0) 
            flag=true; 

          return flag; 
        } 
      } 

注意

使用 DAO 类时,依赖将通过构造函数注入。

我们之前在几页中讨论了关于简短处理异常的内容。让我们更详细地了解它。JDBC 代码强制通过检查异常处理异常。但是,它们是泛化的,并且仅通过 DataTrucationException,SQLException,BatchUpdateException,SQLWarning 处理。与 JDBC 相反,Spring 支持各种未检查异常,为不同场景提供专门的信息。以下表格显示了我们可能需要频繁使用的其中一些:

Spring 异常 它们什么时候被抛出?
数据访问异常 这是 Spring 异常层次结构的根,我们可以将其用于所有情况。
权限被拒数据访问异常 当尝试在没有正确授权的情况下访问数据时
空结果数据访问异常 从数据库中没有返回任何行,但至少期望有一个。
结果大小不匹配数据访问异常 当结果大小与期望的结果大小不匹配时。
类型不匹配数据访问异常 Java 和数据库之间的数据类型不匹配。
无法获取锁异常 在更新数据时未能获取锁
数据检索失败异常 当使用 ORM 工具通过 ID 搜索和检索数据时

在使用 Spring DataSource,模板类,DAOSupport 类处理数据库操作时,我们仍然涉及使用 SQL 查询进行 JDBC 操作,而不进行面向对象的操作。处理数据库操作的最简单方法是使用对象关系映射将对象置于中心。

对象关系映射


JDBC API 提供了执行关系数据库操作的手段以实现持久化。Java 开发人员积极参与编写 SQL 查询以进行此类数据库操作。但是 Java 是一种面向对象编程语言(OOP),而数据库使用 顺序查询语言SQL)。OOP 的核心是对象,而 SQL 有数据库。OOP 没有主键概念,因为它有身份。OOP 使用继承,但 SQL 没有。这些以及许多其他不匹配之处使得没有深入了解数据库及其结构的情况下,JDBC 操作难以执行。一些优秀的 ORM 工具已经提供了解决方案。ORM 处理数据库操作,将对象置于核心位置,而无需开发者处理 SQL。市场上的 iBATIS、JPA 和 Hibernate 都是 ORM 框架的例子。

Hibernate

Hibernate 是开发者在 ORM 解决方案中使用的高级中间件工具之一。它以一种简单的方式提供了细粒度、继承、身份、关系关联和导航问题的解决方案。开发者无需手动编写 SQL 查询,因为 Hibernate 提供了丰富的 API 来处理 CRUD 数据库操作,使得系统更易于维护和开发。SQL 查询依赖于数据库,但在 Hibernate 中无需编写 SQL 语句,因为它提供了数据库无关性。它还支持 Hibernate 查询语言(HQL)和原生 SQL 支持,通过编写查询来自定义数据库操作。使用 Hibernate,开发者可以缩短开发时间,从而提高生产力。

Hibernate 架构

下面的图表展示了 Hibernate 的架构及其中的接口:

Hibernate 拥有 Session、SessionFactory、Configuration、Transaction、Query 和 Criteria 接口,为核心提供了 ORM 支持,帮助开发者进行对象关系映射。

Configuration 接口

使用 Configuration 实例来指定数据库属性,如 URL、用户名和密码,映射文件的路径或包含数据成员与表及其列映射信息的类。这个 Configuration 实例随后用于获取 SessionFactory 的实例。

SessionFactory 接口

SessionFactory 是重量级的,每个应用程序通常只有一个实例。但是有时一个应用程序会使用多个数据库,这就导致每个数据库都有一个实例。SessionFactory 用于获取 Session 的实例。它非常重要,因为它缓存了生成的 SQL 语句和数据,这些是 Hibernate 在一个工作单元内运行时使用的,作为第一级缓存。

Session 接口

Session 接口是每个使用 Hibernate 的应用程序用来执行数据库操作的基本接口,这些接口是从 SessionFactory 获取的。Session 是轻量级、成本低的组件。因为 SessionFactory 是针对每个应用程序的,所以开发者为每个请求创建一个 Session 实例。

Transaction 接口

事务帮助开发人员将多个操作作为工作单元。JTA、JDBC 提供事务实现的实现。除非开发者提交事务,否则数据不会反映在数据库中。

查询接口

查询接口提供使用 Hibernate 查询语言(HQL)或原生 SQL 来执行数据库操作的功能。它还允许开发人员将值绑定到 SQL 参数,指定查询返回多少个结果。

条件接口

条件接口与查询接口类似,允许开发人员编写基于某些限制或条件的条件查询对象以获取结果。

在 Spring 框架中,开发者可以选择使用 SessionFactory 实例或 HibernateTemplate 进行 Hibernate 集成。SessionFactory 从数据库连接参数配置和映射位置获取,然后使用 DI 可以在 Spring 应用程序中使用。SessionFactory可以如下配置:

<bean id="sessionFactory" 
    class="org.springframework.orm.hibernate5.LocalSessionFactoryBean"> 
    <property name="dataSource" ref="dataSource" /> 
    <property name="mappingResources"> 
      <list> 
        <value>book.hbm.xml</value> 
      </list> 
    </property> 
    <property name="hibernateProperties"> 
      <props> 
        <prop key=    
          "hibernate.dialect">org.hibernate.dialect.MySQLDialect 
        </prop> 
        <prop key="hibernate.show_sql">true</prop> 
        <prop key="hibernate.hbm2ddl.auto">update</prop> 
      </props> 
    </property> 
  </bean> 

  • dataSource - 提供数据库属性的信息。

  • mappingResource - 指定提供数据成员到表及其列映射信息的文件名称。

  • hibernateProperties - 提供关于 hibernate 属性的信息

方言 - 它用于生成符合底层数据库的 SQL 查询。

show_sql - 它显示框架在控制台上发出的 SQL 查询。

hbm2ddl.auto - 它提供了是否创建、更新表以及要执行哪些操作的信息。

在使用 SessionFactory 时,开发人员不会编写使用 Spring API 的代码。但我们之前已经讨论过模板类。HibenateTemplate 是这样一个模板,它帮助开发人员编写松耦合的应用程序。HibernateTemplate 配置如下:

<bean id="hibernateTemplate" \
  class="org.springframework.orm.hibernate5.HibernateTemplate"> 
    <property name="sessionFactory" ref="sessionFactory"></property> 
</bean> 

让我们按照以下步骤逐一将 SessionFactory 集成到我们的 Book 项目中。

案例 1:使用 SessionFactory
  1. 创建一个名为 Ch03_Spring_Hibernate_Integration 的 Java 应用程序,并添加 Spring、JDBC 和 hibernate 的 jar 文件,如下 outline 所示:

  • 您可以从 Hibernate 的官方网站下载包含 hibernate 框架 jar 的 zip 文件。
  1. 在 com.packt.ch03.beans 包中复制或创建 Book.java。

  2. 在类路径中创建 book.hbm.xml,将 Book 类映射到 book_hib 表,如下配置所示:

      <hibernate-mapping> 
        <class name="com.packt.ch03.beans.Book" table="book_hib"> 
          <id name="ISBN" type="long"> 
            <column name="ISBN" /> 
            <generator class="assigned" /> 
          </id> 
          <property name="bookName" type="java.lang.String"> 
            <column name="book_name" /> 
          </property>               
          <property name="description" type="java.lang.String"> 
            <column name="description" /> 
          </property> 
          <property name="author" type="java.lang.String"> 
            <column name="book_author" /> 
          </property> 
          <property name="price" type="int"> 
            <column name="book_price" /> 
          </property> 
        </class> 

      </hibernate-mapping>
  • 其中标签配置如下:

id - 定义从表到图书类的主键映射

属性 - 提供数据成员到表中列的映射

  1. 像在 Ch03_JdbcTemplate 应用程序中一样添加 BookDAO 接口。

  2. 通过 BookDAO_SessionFactory 实现 BookDAO 并覆盖方法。用@Repository 注解类。添加一个类型为 SessionFactory 的数据成员,并用@Autowired 注解。代码如下所示:

      @Repository(value = "bookDAO_sessionFactory") 
      public class BookDAO_SessionFactory implements BookDAO { 

        @Autowired 
        SessionFactory sessionFactory; 

        @Override 
        public int addBook(Book book) { 
          // TODO Auto-generated method stub 
          Session session = sessionFactory.openSession(); 
          Transaction transaction = session.beginTransaction(); 
          try { 
            session.saveOrUpdate(book); 
            transaction.commit(); 
            session.close(); 
            return 1; 
          } catch (DataAccessException exception) { 
            exception.printStackTrace(); 
          } 
          return 0; 
        } 

        @Override 
        public int updateBook(long ISBN, int price) { 
          // TODO Auto-generated method stub 
          Session session = sessionFactory.openSession(); 
          Transaction transaction = session.beginTransaction(); 
          try { 
            Book book = session.get(Book.class, ISBN); 
            book.setPrice(price); 
            session.saveOrUpdate(book); 
            transaction.commit(); 
            session.close(); 
            return 1; 
          } catch (DataAccessException exception) { 
            exception.printStackTrace(); 
          } 
          return 0; 
        } 

        @Override 
        public boolean deleteBook(long ISBN) { 
          // TODO Auto-generated method stub 
          Session session = sessionFactory.openSession(); 
          Transaction transaction = session.beginTransaction(); 
          try { 
            Book book = session.get(Book.class, ISBN); 
            session.delete(book); 
            transaction.commit(); 
            session.close(); 
            return true; 
          } catch (DataAccessException exception) { 
            exception.printStackTrace(); 
          } 
          return false; 
        } 
      } 

  1. 添加 connection_new.xml 以配置 SessionFactory 和其他详细信息,如下所示:
      <context:annotation-config /> 
      <context:component-scan base-package="com.packt.ch03.*"> 
      </context:component-scan> 

      <bean id="dataSource" 
        class="org.springframework.jdbc.datasource. 
        DriverManagerDataSource"> 
        <!-properties for dataSourceà 
      </bean> 

      <bean id="sessionFactory" class=  
        "org.springframework.orm.hibernate5.LocalSessionFactoryBean"> 
        <property name="dataSource" ref="dataSource" /> 
        <property name="mappingResources"> 
          <list> 
            <value>book.hbm.xml</value> 
          </list> 
        </property> 
        <property name="hibernateProperties"> 
          <props> 
            <prop key=      
              "hibernate.dialect">org.hibernate.dialect.MySQLDialect 
            </prop> 
            <prop key="hibernate.show_sql">true</prop> 
            <prop key="hibernate.hbm2ddl.auto">update</prop> 
          </props> 
        </property> 
      </bean> 

  1. 将 MainBookDAO_operations.java 创建或复制以获取 bean 'bookDAO_sessionFactory'以测试应用程序。代码将是:
      public static void main(String[] args) { 
       // TODO Auto-generated method stub 
       ApplicationContext context=new  
         ClassPathXmlApplicationContext("connection_new.xml"); 
       BookDAO bookDAO=(BookDAO)  
         context.getBean("bookDAO_sessionFactory"); 
       //add book
       int rows=bookDAO.addBook(new Book("Java EE 7 Developer  
         Handbook", 97815674L,"PacktPub  
         publication",332,"explore the Java EE7  
         programming","Peter pilgrim")); 
       if(rows>0) 
       { 
         System.out.println("book inserted successfully"); 
       } 
       else
        System.out.println("SORRY!cannot add book"); 

      //update the book
      rows=bookDAO.updateBook(97815674L,432); 
      if(rows>0) 
      { 
        System.out.println("book updated successfully"); 
      }
      else
        System.out.println("SORRY!cannot update book"); 
        //delete the book
        boolean deleted=bookDAO.deleteBook(97815674L); 
        if(deleted) 
        { 
          System.out.println("book deleted successfully"); 
        }
        else
          System.out.println("SORRY!cannot delete book"); 
      } 

我们已经看到了如何配置 HibernateTemplate 在 XML 中。它与事务广泛工作,但我们还没有讨论过什么是事务,它的配置以及如何管理它?我们将在接下来的几章中讨论它。

实时应用在每个步骤中处理大量数据。比如说我们想要找一本书。使用 hibernate 我们只需调用一个返回书籍的方法,这个方法取决于书籍的 ISBN。在日常生活中,这本书会被搜索无数次,每次数据库都会被访问,导致性能问题。相反,如果有一种机制,当再次有人索求这本书时,它会使用前一次查询的结果,那就太好了。Spring 3.1 引入了有效且最简单的方法——'缓存'机制来实现它,并在 4.1 中添加了 JSR-107 注解支持。缓存的结果将存储在缓存库中,下次将用于避免不必要的数据库访问。你可能想到了缓冲区,但它与缓存不同。缓冲区是用于一次性写入和读取数据的临时中间存储。但缓存是为了提高应用程序的性能而隐藏的,数据在这里被多次读取。

缓存库是对象从数据库获取后保存为键值对的位置。Spring 支持以下库,

基于 JDK 的 ConcurrentMap 缓存:

在 JDK ConcurrentMap 中用作后端缓存存储。Spring 框架具有 SimpleCacheManager 来获取缓存管理器并给它一个名称。这种缓存最适合相对较小的数据,这些数据不经常更改。但它不能用于 Java 堆之外存储数据,而且没有内置的方法可以在多个 JVM 之间共享数据。

基于 EhCache 的缓存:

EhcacheChacheManager 用于获取一个缓存管理器,其中配置 Ehcache 配置规范,通常配置文件名为 ehcache.xml。开发者可以为不同的数据库使用不同的缓存管理器。

Caffeine 缓存:

Caffeine 是一个基于 Java8 的缓存库,提供高性能。它有助于克服 ConcurrentHashMap 的重要缺点,即它直到显式移除数据才会持久化。除此之外,它还提供数据的自动加载、基于时间的数据过期以及被驱逐的数据条目的通知。

Spring 提供了基于 XML 以及注解的缓存配置。最简单的方法是使用注解 based 配置。从 Spring 3.1 开始,版本已启用 JSR-107 支持。为了利用 JSR-107 的缓存,开发人员需要首先进行缓存声明,这将帮助识别要缓存的方法,然后配置缓存以通知数据存储在哪里。

缓存声明

缓存声明可以使用注解以及基于 XML 的方法。以下开发人员可以使用注解进行声明:

@Cacheable:

该注解用于声明这些方法的结果将被存储在缓存中。它带有与之一致的缓存名称。每次开发人员调用方法时,首先检查缓存以确定调用是否已经完成。

@Caching:

当需要在同一个方法上嵌套多个 @CacheEvict@CachePut 注解时使用该注解。

@CacheConfig:

使用注解 @CacheConfig 来注解类。对于使用基于缓存的注解 annotated 的类方法,每次指定缓存名称。如果类有多个方法,则使用 @CacheConfig 注解允许我们只指定一次缓存名称。

@CacheEvict:

用于从缓存区域删除未使用数据。

@CachePut

该注解用于在每次调用被其注解的方法时更新缓存结果。该注解的行为与 @Cacheable 正好相反,因为它强制调用方法以更新缓存,而 @Cacheable 跳过执行。

缓存配置:

首先,为了启用基于注解的配置,Spring 必须使用缓存命名空间进行注册。以下配置可用于声明缓存命名空间并注册注解:

<beans xmlns="http://www.springframework.org/schema/beans" 
  xmlns:cache="http://www.springframework.org/schema/cache" 
  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  
  http://www.springframework.org/schema/cache 
  http://www.springframework.org/schema/cache/spring-cache.xsd"> 
        <cache:annotation-driven /> 
</beans> 

注册完成后,现在是提供配置以指定存储库的名称以及使用哪个缓存管理器存储库结果的时候了。我们将在 SimpleCacheManager 的示例演示中很快定义这些配置。

让我们在我们的 Book 应用程序中集成 JDK 基于的 ConcurrentMap 存储库。我们将使用 Ch03_Spring_Hibernate_Integration 作为演示的基础项目。按照集成的步骤操作,

  1. 创建一个名为 Ch03_CacheManager 的新 Java 应用程序,并添加 Spring、JDBC 和 hibernate 的 jar 文件。你也可以参考 Ch03_Spring_Hibernate_Integration 应用程序。

  2. com.packt.ch03.beans 包中创建或复制 Book.java 文件。

  3. com.packt.ch03.dao 包中创建或复制 BookDAO 接口,并向其添加一个使用 ISBN 从数据库中搜索书籍的方法。该方法的签名如下所示:

      public Book getBook(long ISBN); 

  1. BookDAO_SessionFactory_Cache 中实现方法,正如我们在 Hibernate 应用程序中的 BookDAO_SessionFactory.java 中所做的那样。从数据库获取书籍的方法将是:
      public Book getBook(long ISBN) { 
        // TODO Auto-generated method stub 
        Session session = sessionFactory.openSession(); 
        Transaction transaction = session.beginTransaction(); 
        Book book = null; 
        try { 
          book = session.get(Book.class, ISBN); 
          transaction.commit(); 
          session.close(); 
        } catch (DataAccessException exception) { 
          exception.printStackTrace(); 
          book; 
      } 

该方法将使用'repo'存储库来缓存结果。

  1. 将 book.hbm.xml 复制到类路径中。

  2. 添加带有主函数的 MainBookDAO_Cache.java,以从数据库获取数据,但故意我们会如下的获取两次数据:

      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context=new  
          ClassPathXmlApplicationContext("connection_new.xml"); 
        BookDAO bookDAO=(BookDAO)   
          context.getBean("bookDAO_sessionFactory"); 
        Book book=bookDAO.getBook(97815674L);    

        System.out.println(book.getBookName()+ 
          "\t"+book.getAuthor()); 
        Book book1=bookDAO.getBook(97815674L); 
        System.out.println(book1.getBookName()+ 
          "\t"+book1.getAuthor()); 
      } 

  1. 在执行之前,请确保我们要搜索的 ISBN 已经存在于数据库中。我们将得到以下输出:
      Hibernate: select book0_.ISBN as ISBN1_0_0_, book0_.book_name as        book_nam2_0_0_, book0_.description as descript3_0_0_,
      book0_.book_author as book_aut4_0_0_, book0_.book_price as
      book_pri5_0_0_ from book_hib book0_ where book0_.ISBN=? 
      book:-Java EE 7 Developer Handbook  Peter pilgrim 

      Hibernate: select book0_.ISBN as ISBN1_0_0_, book0_.book_name as        book_nam2_0_0_, book0_.description as descript3_0_0_,  
      book0_.book_author as book_aut4_0_0_, book0_.book_price as 
      book_pri5_0_0_ from book_hib book0_ where book0_.ISBN=? 
      book1:-Java EE 7 Developer Handbook  Peter pilgrim 

上述输出清楚地显示了搜索书籍的查询执行了两次,表示数据库被访问了两次。

现在让我们配置 Cache manager 以缓存搜索书籍的结果,如下所示,

  1. 使用@Cacheable 注解来标记那些结果需要被缓存的方法,如下所示:
      @Cacheable("repo") 
      public Book getBook(long ISBN) {// code will go here } 

  1. 在 connection_new.xml 中配置缓存命名空间,正如我们已经在讨论中提到的。

  2. 在 XML 中注册基于注解的缓存,如下所示:

      <cache:annotation-driven /> 

  1. 为设置存储库为'repo'添加 CacheManger,如下配置所示:
      <bean id="cacheManager"  
        class="org.springframework.cache.support.SimpleCacheManager"> 
        <property name="caches"> 
          <set> 
            <bean class="org.springframework.cache.concurrent.
              ConcurrentMapCache FactoryBean"> 
              <property name="name" value="repo"></property> 
            </bean> 
          </set> 
        </property> 
      </bean> 

  1. 不更改地执行 MainBookDAO_Cache.java 以得到以下输出:
      Hibernate: select book0_.ISBN as ISBN1_0_0_, book0_.book_name as        book_nam2_0_0_, book0_.description as descript3_0_0_,  
      book0_.book_author as book_aut4_0_0_, book0_.book_price as 
      book_pri5_0_0_ from book_hib book0_ where book0_.ISBN=? 
      book:-Java EE 7 Developer Handbook  Peter pilgrim 

      book1:-Java EE 7 Developer Handbook  Peter pilgrim 

控制台输出显示,即使我们两次搜索了书籍,查询也只执行了一次。由getBook()第一次获取的书籍结果被缓存起来,下次有人请求这本书而没有加热数据库时,会使用这个缓存结果。

总结


在本章中,我们深入讨论了持久层。讨论使我们了解了如何通过 Spring 使用 DataSource 将 JDBC 集成到应用程序中。但使用 JDBC 仍然会让开发者接触到 JDBC API 及其操作,如获取 Statement、PreparedStatement 和 ResultSet。但 JdbcTemplate 和 JdbcDaoSupport 提供了一种在不涉及 JDBC API 的情况下执行数据库操作的方法。我们还看到了 Spring 提供的异常层次结构,可以根据应用程序的情况使用它。我们还讨论了 Hibernate 作为 ORM 工具及其在框架中的集成。缓存有助于最小化对数据库的访问并提高性能。我们讨论了缓存管理器以及如何将 CacheManger 集成到应用程序中。

在下一章中,我们将讨论面向方面的编程,它有助于处理交叉技术。

第四章。面向切面编程

上一章关于 Spring DAO 的内容为我们提供了很好的实践,了解了 Spring 如何通过松耦合方式处理 JDBC API。但是,我们既没有讨论 JDBC 事务,也没有探讨 Spring 如何处理事务。如果你已经处理过事务,你了解其步骤,而且更加清楚这些步骤是重复的,并且分散在代码各处。一方面,我们提倡使用 Spring 来避免代码重复,另一方面,我们却在编写这样的代码。Java 强调编写高内聚的模块。但是在我们的代码中编写事务管理将不允许我们编写内聚的模块。此外,编写代码的目的并非是为了事务。它只是提供支持,以确保应用程序的业务逻辑不会产生任何不期望的效果。我们还没有讨论过如何处理这种支持功能以及应用程序开发的主要目的。除了事务之外,还有哪些功能支持应用程序的工作?本章将帮助我们编写没有代码重复的高度内聚的模块,以处理这些支持功能。在本章中,我们将讨论以下几点:

  • 交叉技术是什么?

  • 交叉技术在应用程序开发中扮演什么角色?

  • 我们将讨论关于面向切面编程(AOP)以及 AOP 在处理交叉技术中的重要作用。

  • 我们将深入探讨 AOP 中的方面、建议和切点是什么。

软件应用程序为客户的问题提供了一个可靠的解决方案。尽管我们说它是可靠的,但总是有可能出现一些运行时问题。因此,在开发过程中,软件的维护同样重要。每当应用程序中出现问题时,客户都会回来找开发者寻求解决方案。除非客户能够准确地说明问题的原因,否则开发者是无能为力的。为了防止问题的再次发生,开发者必须重新创建相同的情况。在企业应用程序中,由于模块数量众多,重新创建相同的问题变得复杂。如果有一个人能够持续跟踪用户在做什么,那就太好了。这个跟踪器的跟踪帮助开发者了解出了什么问题,以及如何轻松地重新创建它。是的,我在谈论日志记录机制。

让我们考虑另一个非常常见的铁路票务预订情况。在票务预订时,我们从图表中选择可用的座位并继续进行资金转账。有时资金成功转账,票也预订了。但不幸的是,有时由于资金交易时间过长,填写表格延迟或一些服务器端问题可能会导致资金转账失败而无法预订票。资金被扣除而没有发行票。客户将不高兴,而且对于退款来说会更加紧张。这种情况需要借助事务管理谨慎处理,以便如果未发行票,资金应退还到客户账户中。手动操作将是繁琐的任务,而事务管理则优雅地处理了这个问题。

我们可以编写不包含日志记录或事务管理的可运行代码,因为这两者都不属于您的业务逻辑。Java 应用程序的核心是提供一种定制化的、简单的解决方案来解决企业问题。业务逻辑位于中心,提供应用程序的主要功能,有时被称为“主要关注点”。但它还必须支持其他一些功能或服务,这一点不容忽视。这些服务在应用程序中扮演着重要的角色。要么应用程序的迁移会耗时,要么在运行时回溯问题将变得困难。这些关注点大多伴随着重复的代码散布在应用程序中。这些次要关注点被称为“横切关注点”,有时也称为“水平关注点”。日志记录、事务管理和安全机制是开发者在应用程序中使用的横切关注点。

下面的图表展示了横切关注点(如日志记录和事务管理)如何在应用程序代码中散布:

面向切面编程(AOP)


与面向对象编程类似,面向切面编程也是一种编程风格,它允许开发者通过将横切关注点与业务逻辑代码分离来编写连贯的代码。AOP 概念是由 Gregor KicZales 及其同事开发的。它提供了编写横切关注点的不同方法或工具。

在 AOP 中处理横切关注点,可以在一个地方编写,从而实现以下好处:

  • 减少代码重复以实现编写整洁的代码。

  • 有助于编写松耦合的模块。

  • 有助于实现高度凝聚的模块。

  • 开发者可以专注于编写业务逻辑。

  • 在不更改现有代码的情况下,轻松更改或修改代码以添加新功能。

要理解 AOP,我们必须了解以下常见术语,没有它们我们无法想象 AOP。

连接点

连接点是应用程序中可以插入方面以执行一些杂项功能的位置,而不会成为实际业务逻辑的一部分。每段代码都有无数的机会,可以被视为连接点。在应用程序中最小的单元类有数据成员、构造函数、设置器和获取器,以及其他功能类。每个都可以是应用方面的机会。Spring 只支持方法作为连接点。

切点(Pointcut)

连接点是应用方面的机会,但并非所有机会都被考虑在内。切点是开发者决定应用方面以对横切关注执行特定动作的地方。切点将使用方法名、类名、正则表达式来定义匹配的包、类、方法,在这些地方可以应用方面。

建议

在切点处方面所采取的动作称为“建议”(advice)。建议包含为相应的横切关注点执行的代码。如果我们把方法作为连接点,方面可以在方法执行之前或之后应用,也可能是方法有异常处理代码,方面可以插入其中。以下是 Spring 框架中可用的建议。

前置(Before)

前置(Before)建议包含在匹配切点表达式的业务逻辑方法执行之前应用的实现。除非抛出异常,否则将继续执行该方法。可以使用@Before 注解或aop:before配置来支持前置建议。

后抛出(After)

在后置(After)建议中,实现将在业务逻辑方法执行之后应用,无论方法执行成功还是抛出异常。可以使用@After 注解或aop:after配置来支持后置建议,将其应用于一个方法。

后返回(After returning)

后返回(After Returning)建议的实现仅在业务逻辑方法成功执行后应用。可以使用@AfterReturning 注解或aop:after-returning配置来支持后返回建议。后返回建议方法可以使用业务逻辑方法返回的值。

后抛出(After throwing)

后抛出(After Throwning)建议的实现应用于业务逻辑方法抛出异常之后。可以使用@AfterThrowing 注解或aop:throwing配置来支持后抛出建议,将其应用于一个方法。

环绕(Around)

环绕通知是所有通知中最重要的一种,也是唯一一种在业务逻辑方法执行前后都会应用的通知。它可用于通过调用 ProceedingJoinPoint 的 proceed()方法来选择是否继续下一个连接点。proceed()通过返回其自身的返回值来帮助选择是否继续到连接点。它可用于开发人员需要执行预处理、后处理或两者的场景。计算方法执行所需时间就是一个这样的场景。可以使用@Around 注解或aop:around配置通过将其应用于一个方法来支持环绕通知。

Aspect(方面)

方面通过切入点表达式和通知来定义机会,以指定动作何时何地被执行。使用@Aspect 注解或aop:aspect配置将一个类声明为方面。

Introduction(介绍)

介绍可以帮助在不需要更改现有代码的情况下,在现有类中声明额外的方法和字段。Spring AOP 允许开发人员向任何被方面通知的类引入新的接口。

Target object(目标对象)

目标对象是被应用了方面的类的对象。Spring AOP 在运行时创建目标对象的代理。从类中覆盖方法并将通知包含进去以获得所需结果。

AOP proxy(AOP 代理)

默认情况下,Spring AOP 使用 JDK 的动态代理来获取目标类的代理。使用 CGLIB 进行代理创建也非常常见。目标对象始终使用 Spring AOP 代理机制进行代理。

Weaving(编织)

我们作为开发者将业务逻辑和方面代码写在两个分开的模块中。然后这两个模块必须合并为一个被代理的目标类。将方面插入业务逻辑代码的过程称为“编织”。编织可以在编译时、加载时或运行时发生。Spring AOP 在运行时进行编织。

让我们通过一个非常简单的例子来理解所讨论的术语。我的儿子喜欢看戏剧。所以我们去看了一场。我们都知道,除非我们有入场券,否则我们不能进入。显然,我们首先需要收集它们。一旦我们有了票,我的儿子把我拉到座位上,兴奋地指给我看。演出开始了。这是一场给孩子们看的有趣戏剧。所有孩子们都在笑笑话,为对话鼓掌,在戏剧场景中感到兴奋。休息时,观众中的大多数人去拿爆米花、小吃和冷饮。每个人都喜欢戏剧,并快乐地从出口离开。现在,我们可能认为我们都知道这些。我们为什么要讨论这个,它与方面有什么关系。我们是不是偏离了讨论的主题?不,我们正在正确的轨道上。再等一会儿,你们所有人也会同意。这里看戏剧是我们的主要任务,让我们说这是我们的业务逻辑或核心关注。购买门票,支付钱,进入剧院,戏剧结束后离开是核心关注的一部分功能。但我们不能安静地坐着,我们对正在发生的事情做出反应?我们鼓掌,笑,有时甚至哭。但这些是主要关注点吗?不!但没有它们,我们无法想象观众看戏剧。这些将是每个观众自发执行的支持功能。正确!!!这些是交叉关注点。观众不会为交叉关注点单独收到指示。这些反应是方面建议的一部分。有些人会在戏剧开始前鼓掌,少数人在戏剧结束后鼓掌,最兴奋的是当他们感到的时候。这只是方面的前置、后置或周围建议。如果观众不喜欢戏剧,他们可能会在中间离开,类似于抛出异常。在非常不幸的日子里,演出可能会被取消,甚至可能在中间停止,需要组织者作为紧急情况介绍。希望现在你知道了这些概念以及它们的实际方法。我们将在演示中简要介绍这些以及更多内容。

在继续演示之前,让我们首先讨论市场上的一些 AOP 框架如下。

AspectJ

AspectJ 是一个易于使用和学习的 Java 兼容框架,用于集成跨切实现的交叉。AspectJ 是在 PARC 开发的。如今,由于其简单性,它已成为一个著名的 AOP 框架,同时具有支持组件模块化的强大功能。它可用于对静态或非静态字段、构造函数、私有、公共或受保护的方法应用 AOP。

AspectWertz

AspectWertz 是另一个与 Java 兼容的轻量级强大框架。它很容易集成到新旧应用程序中。AspectWertz 支持基于 XML 和注解的方面编写和配置。它支持编译时、加载时和运行时编织。自 AspectJ5 以来,它已被合并到 AspectJ 中。

JBoss AOP

JBoss AOP 支持编写方面以及动态代理目标对象。它可以用于静态或非静态字段、构造函数、私有、公共或受保护的方法上使用拦截器。

Dynaop

Dynaop 框架是一个基于代理的 AOP 框架。该框架有助于减少依赖性和代码的可重用性。

CAESAR

CASER 是一个与 Java 兼容的 AOP 框架。它支持实现抽象组件以及它们的集成。

Spring AOP

这是一个与 Java 兼容、易于使用的框架,用于将 AOP 集成到 Spring 框架中。它提供了与 Spring IoC 紧密集成的 AOP 实现,是基于代理的框架,可用于方法执行。

Spring AOP 满足了大部分应用交叉关注点的需求。但以下是一些 Spring AOP 无法应用的限制,

  • Spring AOP 不能应用于字段。

  • 我们无法在一个方面上应用任何其他方面。

  • 私有和受保护的方法不能被建议。

  • 构造函数不能被建议。

Spring 支持 AspectJ 和 Spring AOP 的集成,以减少编码实现交叉关注点。Spring AOP 和 AspectJ 都用于实现交叉技术,但以下几点有助于开发者在实现时做出最佳选择:

  • Spring AOP 基于动态代理,支持方法连接点,但 AspectJ 可以应用于字段、构造函数,甚至是私有、公共或受保护的,支持细粒度的建议。

  • Spring AOP 不能用于调用同一类方法的方法、静态方法或最终方法,但 AspectJ 可以。

  • AspectJ 不需要 Spring 容器来管理组件,而 Spring AOP 只能用于由 Spring 容器管理的组件。

  • Spring AOP 支持基于代理模式的运行时编织,而 AspectJ 支持编译时编织,不需要创建代理。对象的代理将在应用程序请求 bean 时创建一次。

  • 由 Spring AOP 编写的方面是基于 Java 的组件,而用 AspectJ 编写的方面是扩展 Java 的语言,所以开发者在使用之前需要学习它。

  • Spring AOP 通过使用@Aspect 注解标注类或简单的配置来实现非常简单。但是,要使用 AspectJ,则需要创建*.aj 文件。

  • Spring AOP 不需要任何特殊的容器,但方面需要使用 AspectJ 编译。

  • AspectJ 是现有应用程序的最佳选择。

    注意

    如果没有 final,静态方法的简单类,则可以直接使用 Spring AOP,否则选择 AspectJ 来编写切面。

让我们深入讨论 Spring AOP 及其实现方式。Spring AOP 可以通过基于 XML 的切面配置或 AspectJ 风格的注解实现。基于 XML 的配置可以分成几个点,使其变得稍微复杂。在 XML 中,我们无法定义命名切点。但由注解编写的切面位于单个模块中,支持编写命名切点。所以,不要浪费时间,让我们开始基于 XML 的切面开发。

基于 XML 的切面配置

以下是在开发基于 XML 的切面时需要遵循的步骤,

  1. 选择要实现的重叠关注点

  2. 编写切面以满足实现重叠关注点的需求。

  3. 在 Spring 上下文中注册切面作为 bean。

  4. 切面配置写为:

  • 在 XML 中添加 AOP 命名空间。

  • 添加切面配置,其中将包含切点表达式和建议。

  • 注册可以应用切面的 bean。

开发人员需要从可用的连接点中决定跟踪哪些连接点,然后需要使用表达式编写切点以针对它们。为了编写这样的切点,Spring 框架使用 AspectJ 的切点表达式语言。我们可以在表达式中使用以下设计器来编写切点。

使用方法签名

可以使用方法签名从可用连接点定义切点。表达式可以使用以下语法编写:

expression(<scope_of_method>    <return_type><fully_qualified_name_of_class>.*(parameter_list)

Java 支持 private,public,protected 和 default 作为方法范围,但 Spring AOP 只支持公共方法,在编写切点表达式时。参数列表用于指定在匹配方法签名时要考虑的数据类型。如果开发人员不想指定参数数量或其数据类型,可以使用两个点(..)。

让我们考虑以下表达式,以深入理解表达式的编写,从而决定哪些连接点将受到建议:

  • expression(* com.packt.ch04.MyClass.*(..)) - 指定 com.packt.cho3 包内 MyClass 的具有任何签名的所有方法。

  • expression(public int com.packt.ch04.MyClass.*(..)) - 指定 com.packt.cho3 包内 MyClass 中返回整数值的所有方法。

  • expression(public int com.packt.ch04.MyClass.*(int,..)) - 指定返回整数及其第一个整数类型参数的 MyClass 中所有方法,该类位于 com.packt.cho3 包内。

  • expression(* MyClass.*(..)) - 指定所有来自 MyClass 的具有任何签名的方法都将受到建议。这是一个非常特殊的表达式,只能在与建议的类在同一包中使用。

使用类型

类型签名用于匹配具有指定类型的连接点。我们可以使用以下语法来指定类型:

within(type_to_specify) 

这里类型将是包或类名。以下是一些可以编写以指定连接点的表达式:

  • within(com.packt.ch04.*) - 指定属于 com.packt.ch04 包的所有类的所有方法

  • within(com.packt.ch04..*) - 指定属于 com.packt.ch04 包及其子包的所有类的所有方法。我们使用了两个点而不是一个点,以便同时跟踪子包。

  • within(com.packt.ch04.MyClass) - 指定属于 com.packt.ch04 包的 MyClass 的所有方法

  • within(MyInterface+) - 指定实现 MyInterface 的所有类的所有方法。

使用 Bean 名称

Spring 2.5 及以后的所有版本都支持在表达式中使用 bean 名称来匹配连接点。我们可以使用以下语法:

bean(name_of_bean) 

考虑以下示例:

bean(*Component) - 这个表达式指定要匹配的连接点属于名称以 Component 结尾的 bean。这个表达式不能与 AspectJ 注解一起使用。

使用 this

'this'用于匹配目标点的 bean 引用是指定类型的实例。当表达式指定类名而不是接口时使用。当 Spring AOP 使用 CGLIB 进行代理创建时使用。

5.sing target

目标用于匹配目标对象是指定类型的接口的连接点。当 Spring AOP 使用基于 JDK 的代理创建时使用。仅当目标对象实现接口时才使用目标。开发者甚至可以配置属性'proxy target class'设置为 true。

让我们考虑以下示例以了解表达式中使用 this 和 target:

package com.packt.ch04; 
Class MyClass implements MyInterface{ 
  // method declaration 
} 

我们可以编写表达式来针对方法:

target( com.packt.ch04.MyInterface)

this(com.packt.ch04.MyClass)

用于注解跟踪

开发者可以编写不跟踪方法而是跟踪应用于注解的连接点表达式。让我们以下示例了解如何监控注解。

使用 with execution:

execution(@com.packt.ch03.MyAnnotation) - 指定被 MyAnnotation 注解标记的方法或类。

execution(@org.springframework.transaction.annotation.Transactional) - 指定被 Transactional 注解标记的方法或类。

使用 with @target:

它用于考虑被特定注解标记的类的连接点。以下示例解释得很清楚,

@target(com.packt.ch03.MyService) - 用于考虑被 MyService 注解标记的连接点。

使用@args:

表达式用于指定参数被给定类型注解的连接点。

@args(com.packt.ch04.annotations.MyAnnotation)

上述表达式用于考虑其接受的对象被@Myannotation 注解标记的连接点。

使用@within:

表达式用于指定由给定注解指定的类型的连接点。

@within(org.springframework.stereotype.Repository)

上述表达式有助于为被@Repository 标记的连接点提供通知。

使用@annotation:

@annotation 用于匹配被相应注解标记的连接点。

@annotation(com.packt.ch04.annotations.Annotation1)

表达式匹配所有由 Annotation1 标记的连接点。

让我们使用切点表达式、通知来实现日志方面,以理解实时实现。我们将使用上一章开发的 Ch03_JdbcTemplates 应用程序作为基础,将其与 Log4j 集成。第一部分我们将创建一个主应用程序的副本,第二部分将其与 log4j 集成,第三部分将应用自定义日志方面。

第一部分:创建核心关注点(JDBC)的应用程序


按照以下步骤创建基础应用程序:

  1. 创建一个名为 Ch04_JdbcTemplate_LoggingAspect 的 Java 应用程序,并添加 Spring 核心、Spring JDBC、spring-aop、aspectjrt-1.5.3 和 aspectjweaver-1.5.3.jar 文件所需的 jar。

  2. 将所需源代码文件和配置文件复制到相应的包中。应用程序的最终结构如下所示:

  1. 从 Ch03_JdbcTemplates 复制 connection_new.xml 到应用程序的类路径中,并编辑它以删除 id 为'namedTemplate'的 bean。

第二部分:Log4J 的集成


Log4j 是最简单的事情。让我们按照以下步骤进行集成:

  1. 要集成 Log4J,我们首先必须将 log4j-1.2.9.jar 添加到应用程序中。

  2. 在类路径下添加以下配置的 log4j.xml 以添加控制台和文件监听器:

      <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> 
      <log4j:configuration  
         xmlns:log4j='http://jakarta.apache.org/log4j/'> 
        <appender name="CA"  
          class="org.apache.log4j.ConsoleAppender"> 
          <layout class="org.apache.log4j.PatternLayout"> 
            <param name="ConversionPattern" value="%-4r [%t]  
              %-5p %c %x - %m%n" /> 
          </layout> 
        </appender> 
        <appender name="file"  
          class="org.apache.log4j.RollingFileAppender"> 
          <param name="File" value="C:\\log\\log.txt" /> 
          <param name="Append" value="true" /> 
          <param name="MaxFileSize" value="3000KB" /> 
          <layout class="org.apache.log4j.PatternLayout"> 
            <param name="ConversionPattern" value="%d{DATE}  
              %-5p %-15c{1}: %m%n" /> 
          </layout> 
        </appender> 
        <root> 
          <priority value="INFO" /> 
          <appender-ref ref="CA" /> 
          <appender-ref ref="file" /> 
        </root> 
      </log4j:configuration> 

您可以根据需要修改配置。

  1. 现在,为了记录消息,我们将添加获取日志记录器和记录机制的代码。我们可以将代码添加到 BookDAO_JdbcTemplate.java,如下所示:
      public class BookDAO_JdbcTemplate implements BookDAO {  
        Logger logger=Logger.getLogger(BookDAO_JdbcTemplate.class); 
        public int addBook(Book book) { 
          // TODO Auto-generated method stub 
          int rows = 0; 
          String INSERT_BOOK = "insert into book  
            values(?,?,?,?,?,?)"; 
          logger.info("adding the book in table"); 

          rows=jdbcTemplate.update(INSERT_BOOK, book.getBookName(),  
            book.getISBN(), book.getPublication(),  
            book.getPrice(), 
            book.getDescription(), book.getAuthor()); 

            logger.info("book added in the table successfully"+  
              rows+"affected"); 
          return rows; 
        } 

不要担心,我们不会在每个类和每个方法中添加它,因为我们已经讨论了复杂性和重复代码,让我们继续按照以下步骤编写日志机制方面,以获得与上面编写的代码相同的结果。

第三部分:编写日志方面。


  1. 在 com.packt.ch04.aspects 包中创建一个名为 MyLoggingAspect 的 Java 类,该类将包含一个用于前置通知的方法。

  2. 在其中添加一个类型为 org.apache.log4j.Logger 的数据成员。

  3. 在其中添加一个 beforeAdvise()方法。方法的签名可以是任何东西,我们在这里添加了一个 JoinPoint 作为参数。使用这个参数,我们可以获取有关方面应用的类的信息。代码如下:

      public class MyLoggingAspect { 
        Logger logger=Logger.getLogger(getClass()); 
        public void beforeAdvise(JoinPoint joinPoint) { 
          logger.info("method will be invoked :- 
            "+joinPoint.getSignature());   
        }       
      } 

  1. 现在必须在 XML 中分三步配置方面:

*****为 AOP 添加命名空间:

      <beans xmlns="http://www.springframework.org/schema/beans"     
        xmlns:aop="http://www.springframework.org/schema/aop"  
        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  
        http://www.springframework.org/schema/aop 
        http://www.springframework.org/schema/aop/spring-aop.xsd">
  1. 现在我们可以使用 AOP 的标签,通过使用'aop'命名空间:

*****添加一个方面 bean。

  1. 在 connection_new.xml 中添加我们想在应用程序中使用的方面的 bean,如下所示:
<bean id="myLogger"
  class="com.packt.ch04.aspects.MyLoggingAspect" />

配置切面。

  1. 每个aop:aspect允许我们在aop:config标签内编写切面。

  2. 每个切面都将有 id 和 ref 属性。'ref'指的是将调用提供建议的方法的 bean。

  3. 为切点表达式配置建议,以及要调用的方法。可以在aop:aspect内使用aop:before标签配置前置建议。

  4. 让我们编写一个适用于'myLogger'切面的前置建议,该建议将在 BookDAO 的 addBook()方法之前调用。配置如下:

      <aop:config>
        <aop:aspect id="myLogger" ref="logging">
          <aop:pointcut id="pointcut1"
            expression="execution(com.packt.ch03.dao.BookDAO.addBook
            (com.packt.ch03.beans.Book))" />
          <aop:before pointcut-ref="pointcut1" 
            method="beforeAdvise"/>
        </aop:aspect>
      </aop:config>
  1. 执行 MainBookDAO_operation.java 以在控制台获得以下输出:
      0 [main] INFO       org.springframework.context.support.ClassPathXmlApplicationContext -       Refreshing       org.springframework.context.support.ClassPathXmlApplicationContext@5      33e64: startup date [Sun Oct 02 23:44:36 IST 2016]; root of       context hierarchy
      66 [main] INFO       org.springframework.beans.factory.xml.XmlBeanDefinitionReader -       Loading XML bean definitions from class path resource       [connection_new.xml]
      842 [main] INFO       org.springframework.jdbc.datasource.DriverManagerDataSource - Loaded       JDBC driver: com.mysql.jdbc.Driver
      931 [main] INFO com.packt.ch04.aspects.MyLoggingAspect - method       will be invoked :-int com.packt.ch03.dao.BookDAO.addBook(Book)
      book inserted successfully
      book updated successfully
      book deleted successfully

BookDAO_JdbTemplate 作为目标对象运行,其代理将在运行时通过编织 addBook()和 beforeAdvise()方法代码来创建。现在既然我们知道了过程,让我们逐一在应用程序中添加不同的切点和建议,并按照以下步骤操作。

注意

可以在同一个连接点上应用多个建议,但为了简单地理解切点和建议,我们将每次保留一个建议,并注释掉已经写入的内容。

添加返回建议。

让我们为 BookDAO 中的所有方法添加后置建议。

  1. 在 MyLoggingAspect 中添加一个后置建议的方法 afterAdvise(),如下所示:
      public void afterAdvise(JoinPoint joinPoint) { 
       logger.info("executed successfully :- 
         "+joinPoint.getSignature()); 
      } 

  1. 配置切点表达式,以目标 BookDAO 类中的所有方法以及在'myLogger'切面中的 connection_new.xml 中的后置建议。
      <aop:pointcut id="pointcut2"   
        expression="execution(com.packt.ch03.dao.BookDAO.*(..))" /> 
      <aop:after pointcut-ref="pointcut2" method="afterAdvise"/>
  1. 执行 MainBookDAO_operations.java 以获得以下输出:
999 [main] INFO com.packt.ch04.aspects.MyLoggingAspect - method will be invoked :-int com.packt.ch03.dao.BookDAO.addBook(Book)
1360 [main] INFO com.packt.ch04.aspects.MyLoggingAspect - executed successfully :-int com.packt.ch03.dao.BookDAO.addBook(Book)
book inserted successfully
1418 [main] INFO com.packt.ch04.aspects.MyLoggingAspect - executed successfully :-int com.packt.ch03.dao.BookDAO.updateBook(long,int)
book updated successfully
1466 [main] INFO com.packt.ch04.aspects.MyLoggingAspect - executed successfully :-boolean com.packt.ch03.dao.BookDAO.deleteBook(long)
book deleted successfully

下划线的语句清楚地表明建议在所有方法之后被调用。

在返回后添加建议。

虽然我们编写了后置建议,但我们无法得到业务逻辑方法返回的值。后返回将帮助我们在以下步骤中获取返回值。

  1. 在 MyLoggingAspect 中添加一个返回建议的方法 returnAdvise(),该方法将在返回后调用。代码如下:
      public void returnAdvise(JoinPoint joinPoint, Object val) { 
        logger.info(joinPoint.getSignature()+ " returning val" + val); 
      } 

参数'val'将持有返回值。

  1. 在'myLogger'下配置建议。我们不需要配置切点,因为我们将会重用已经配置的。如果你想要使用不同的连接点集,首先你需要配置一个不同的切点表达式。我们的配置如下所示:
      <aop:after-returning pointcut-ref="pointcut2"
        returning="val" method="returnAdvise" />

其中,

返回-表示要指定返回值传递到的参数的名称。在我们这个案例中,这个名称是'val',它已在建议参数中绑定。

  1. 为了使输出更容易理解,注释掉前置和后置建议配置,然后执行 MainBookDAO_operations.java 以在控制台输出获得以下行:
      1378 [main] INFO  com.packt.ch04.aspects.MyLoggingAspect  - int       com.packt.ch03.dao.BookDAO.addBook(Book)  
      returning val:-1 
      1426 [main] INFO  com.packt.ch04.aspects.MyLoggingAspect  - int       com.packt.ch03.dao.BookDAO.updateBook(long,int) returning val:-1 
      1475 [main] INFO  com.packt.ch04.aspects.MyLoggingAspect  -
      boolean com.packt.ch03.dao.BookDAO.deleteBook(long)
      returning val:-true 

每个语句显示了连接点的返回值。

添加环绕建议。

如我们之前讨论的,环绕建议在业务逻辑方法前后调用,只有当执行成功时。让我们在应用程序中添加环绕建议:

  1. MyLoggingAspect中添加一个aroundAdvise()方法。该方法必须有一个参数是ProceedingJoinPoint,以方便应用程序流程到达连接点。代码如下:

proceed()之前的部分将在我们称为'Pre processing'的 B.L.方法之前被调用。ProceedingJoinPointproceed()方法将流程导向相应的连接点。如果连接点成功执行,将执行proceed()之后的部分,我们称之为'Post processing'。在这里,我们通过在'Pre processing'和'Post processing'之间取时间差来计算完成过程所需的时间。

我们想要编织切面的连接点返回 int,因此 aroundAdvise()方法也返回相同类型的值。如果万一我们使用 void 而不是 int,我们将得到以下异常:

      Exception in thread "main" 
      org.springframework.aop.AopInvocationException: Null return value       from advice does not match primitive return type for: public   
      abstract int  
      com.packt.ch03.dao.BookDAO.addBook(com.packt.ch03.beans.Book) 

  1. 现在让我们在'myLogger'中添加 around advice,如下所示:
      <aop:around pointcut-ref="pointcut1" method="aroundAdvise" />
  1. 在注释掉之前配置的 advice 的同时,在控制台执行MainBookDAO以下日志,
      1016 [main] INFO com.packt.ch04.aspects.MyLoggingAspect - around       advise before int com.packt.ch03.dao.BookDAO.addBook(Book)  
      B.L.method getting invoked
      1402 [main] INFO com.packt.ch04.aspects.MyLoggingAspect - number       of rows affected:-1
      1402 [main] INFO com.packt.ch04.aspects.MyLoggingAspect - around
      advise after int com.packt.ch03.dao.BookDAO.addBook(Book)
      B.L.method getting invoked
      1403 [main] INFO com.packt.ch04.aspects.MyLoggingAspect - int
      com.packt.ch03.dao.BookDAO.addBook(Book) took 388 to complete

添加 after throwing advice

正如我们所知,一旦匹配的连接点抛出异常,after throwing advice 将被触发。在执行 JDBC 操作时,如果我们尝试在 book 表中添加重复的条目,将抛出 DuplicateKeyException,我们只需要使用以下步骤,借助 after throwing advice 进行日志记录:

  1. MyLoggingAspect中添加throwingAdvise()方法,如下所示:
      public void throwingAdvise(JoinPoint joinPoint,  
        Exception exception) 
      { 
        logger.info(joinPoint.getTarget().getClass().getName()+"  
          got and exception" + "\t" + exception.toString()); 
      } 

开发人员可以自由选择签名,但由于连接点方法将抛出异常,为 advice 编写的方法将有一个参数是 Exception 类型,这样我们就可以记录它。我们还在参数中添加了 JoinPoint 类型,因为我们想要处理方法签名。

  1. 在'myLogger'配置中的connection_new.xml中添加配置。要添加的配置是:
      <aop:after-throwing pointcut-ref="pointcut1"
        method="throwingAdvise" throwing="exception" />

aop:after-throwing 将采取:

  • pointcut-ref - 我们想要编织连接点的 pointcut-ref 的名称。

  • method - 如果抛出异常,将调用的方法名称。

  • throwing - 从 advise 方法签名绑定到的参数名称,异常将被传递给它。我们使用的签名中的参数名称是'exception'。

  1. 执行MainBookDAO_operations,并故意添加一个 ISBN 已存在于 Book 表中的书籍。在执行前,注释掉为其他 advice 添加的先前配置。我们将得到以下输出:
      1322 [main] ERROR com.packt.ch04.aspects.MyLoggingAspect  - int 
      com.packt.ch03.dao.BookDAO.addBook(Book) got and exception  
      org.springframework.dao.DuplicateKeyException: 
      PreparedStatementCallback; SQL [insert into book 
      values(?,?,?,?,?,?)]; Duplicate entry '9781235' for key 1; nested 
      exception is 
      com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolation
      Exception: Duplicate entry '9781235' for key 1 

  1. 如果您使用不同的 ISBN 添加书籍,该 ISBN 不在 book 表中,上述 ERROR 日志将不会显示,因为没有异常,也没有 advice 会被触发。

上述示例清楚地展示了如何使用 XML 编写和配置切面。接下来,让我们来编写基于注解的切面。

基于注解的切面。


方面可以声明为用 AspectJ 注解支持的 Java 类,以支持编写切点和建议。Spring AspectJ OP 实现提供了以下注解,用于编写方面:

  • @Aspect - 用于将 Java 类声明为方面。

  • @Pointcut - 使用 AspectJ 表达式语言声明切点表达式。

  • @Before - 用于声明在业务逻辑(B.L.)方法之前应用的前置建议。@Before 支持以下属性,

    • value - 被@Pointcut 注解的方法名称

    • argNames - 指定连接点处的参数名称

  • @After - 用于声明在 B.L.方法返回结果之前应用的后建议。@After 也支持与@Before 建议相同的属性。

  • @AfterThrowing - 用于声明在 B.L.方法抛出异常之后应用的后抛出建议。@AfterThrowing 支持以下属性:

    • pointcut- 选择连接点的切点表达式

    • throwing- 与 B.L.方法抛出的异常绑定在一起的参数名称。

  • @AfterReturning - 用于声明在 B.L.方法返回结果之前但返回结果之后应用的后返回建议。该建议有助于从 B.L.方法获取返回结果的值。@AfterReturning 支持以下属性,

    • pointcut- 选择连接点的切点表达式

    • returning- 与 B.L.方法返回的值绑定的参数名称。

  • @Around - 用于声明在 B.L.方法之前和之后应用的环绕建议。@Around 支持与@Before 或@After 建议相同的属性。

我们必须在 Spring 上下文中声明配置,以禁用 bean 的代理创建。AnnotationAwareAspectJAutoproxyCreator 类在这方面有帮助。我们可以通过在 XML 文件中包含以下配置来简单地为@AspectJ 支持注册类:

<aop:aspectj-autoproxy/> 

在 XML 中添加命名空间'aop',该命名空间已经讨论过。

我们可以按照以下步骤声明和使用基于注解的方面:

  1. 声明一个 Java 类,并用@Aspect 注解它。

  2. 添加被@Pointcut 注解的方法以声明切点表达式。

  3. 根据需求添加建议的方法,并用@Before、@After、@Around 等注解它们。

  4. 为命名空间'aop'添加配置。

  5. 在配置中作为 bean 添加方面。

  6. 在配置中禁用自动代理支持。

让我们在 JdbcTemplate 应用程序中添加基于注解的方面。按照第一部分和第二部分步骤创建名为 Ch04_JdbcTemplate_LoggingAspect_Annotation 的基础应用程序。您可以参考 Ch04_JdbcTemplate_LoggingAspect 应用程序。现在使用以下步骤开发基于注解的日志方面:

  1. 在 com.packt.ch04.aspects 包中创建 MyLoggingAspect 类。

  2. 用@Aspect 注解它。

  3. 在其中添加类型为 org.apache.log4j.Logger 的数据成员。

  4. 为应用建议之前的业务逻辑方法 addBook()添加 beforeAdvise()方法。用@Before 注解它。代码如下所示:

      @Aspect 
      public class MyLoggingAspect {
        Logger logger=Logger.*getLogger*(getClass());
        @Before("execution(*  
          com.packt.ch03.dao.BookDAO.addBook(
          com.packt.ch03.beans.Book))") 

        public void beforeAdvise(JoinPoint joinPoint) {
          logger.info("method will be invoked :- 
          "+joinPoint.getSignature()); 
        }
      }
  1. 如果你还没有做过,编辑 connection_new.xml 以添加'aop'命名空间。

  2. 如下的示例中添加 MyLoggingAspect 的 bean:

      <bean id="logging" 
        class="com.packt.ch04.aspects.MyLoggingAspect" />

上述配置的替代方案是通过使用@Component 注解来注释 MyLoggingAspect。

  1. 通过在 connection_new.xml 中添加配置来禁用 AspectJ 自动代理,如下所示:
      <aop:aspectj-autoproxy/>
  1. 运行 MainBookDAO-operation.java 以在控制台获取日志:
      23742 [main] INFO  com.packt.ch04.aspects.MyLoggingAspect  - 
      method will be invoked :-int 
      com.packt.ch03.dao.BookDAO.addBook(Book) 

为每个建议编写切点表达式可能是一个繁琐且不必要的重复任务。我们可以在标记方法中单独声明切点,如下所示:

      @Pointcut(value="execution(* 
      com.packt.ch03.dao.BookDAO.addBook(com.packt.ch03.beans.Book))") 
        public void selectAdd(){} 

然后从建议方法中引用上述内容。我们可以将 beforeAdvise()方法更新为:

      @Before("selectAdd()") 
      public void beforeAdvise(JoinPoint joinPoint) { 
        logger.info("method will be invoked :- 
        "+joinPoint.getSignature()); 
      }
  1. 一旦我们了解了方面声明的基础,接下来让我们为其他方面和切点添加方法,这些已经在方面声明中使用 XML 讨论过了。方面将如下所示:
      @Aspect 
      public class MyLoggingAspect { 

        Logger logger=Logger.getLogger(getClass()); 
        @Pointcut(value="execution(*com.packt.ch03.dao.BookDAO.addBook(
        com.packt.ch03.beans.Book))") 
        public void selectAdd(){   } 

        @Pointcut(value="execution(*   
          com.packt.ch03.dao.BookDAO.*(..))")

        public void selectAll(){    } 

        // old configuration
        /*
        @Before("execution(* 
        com.packt.ch03.dao.BookDAO.addBook(
        com.packt.ch03.beans.Book))")
        public void beforeAdvise(JoinPoint joinPoint) {
          logger.info("method will be invoked :-
          "+joinPoint.getSignature());
        }
        */
        @Before("selectAdd()") 
        public void beforeAdvise(JoinPoint joinPoint) { 
          logger.info("method will be invoked :- 
          "+joinPoint.getSignature()); 
        }
        @After("selectAll()") 
        public void afterAdvise(JoinPoint joinPoint) { 
          logger.info("executed successfully :- 
          "+joinPoint.getSignature()); 
        }
        @AfterThrowing(pointcut="execution(*
          com.packt.ch03.dao.BookDAO.addBook(
          com.packt.ch03.beans.Book))",  
          throwing="exception") 
        public void throwingAdvise(JoinPoint joinPoint,
          Exception exception)
        {
          logger.error(joinPoint.getSignature()+" got and exception"  
            + "\t" + exception.toString()); 
        }
        @Around("selectAdd()") 
        public int aroundAdvise(ProceedingJoinPoint joinPoint) { 
          long start_time=System.*currentTimeMillis*();
          logger.info("around advise before
          "+joinPoint.getSignature()
          +" B.L.method getting invoked");
        Integer o=null;
        try {
          o=(Integer)joinPoint.proceed();
          logger.info("number of rows affected:-"+o);
        } catch (Throwable e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        }
        logger.info("around advise after
        "+joinPoint.getSignature()+
        " B.L.method getting invoked");
        long end_time=System.*currentTimeMillis*();
        logger.info(joinPoint.getSignature()+" took " +
        (end_time-start_time)+" to complete");
        return o.intValue();  } 

        @AfterReturning(pointcut="selectAll()", returning="val") 
        public void returnAdvise(JoinPoint joinPoint, Object val) { 
          logger.info(joinPoint.getSignature()+
          " returning val:-" + val); 
        }
      }
  1. 运行 MainBookDAO.java 以在控制台获取日志消息。

默认情况下,JDK 的动态代理机制将用于创建代理。但是有时目标对象没有实现接口,JDK 的代理机制将失败。在这种情况下,可以使用 CGLIB 来创建代理。为了启用 CGLIB 代理,我们可以编写以下配置:

<aop:config proxy-target-class="true"> 
  <!-aspect configuration à 
</aop:config> 

此外,为了强制使用 AspectJ 和自动代理支持,我们可以编写以下配置:

<aop:aspect-autoproxy proxy-target-=class="true"/> 

引入


在企业应用程序中,有时开发者会遇到需要引入一组新功能,但又不改变现有代码的情况。使用引入不一定需要改变所有的接口实现,因为这会变得非常复杂。有时开发者会与第三方实现合作,而源代码不可用,引入起到了非常重要的作用。开发者可能有使用装饰器或适配器设计模式的选项,以便引入新功能。但是,方法级 AOP 可以帮助在不编写装饰器或适配器的情况下实现新功能的引入。

引入是一种顾问,它允许在处理交叉关注点的同时引入新的功能。开发者必须使用基于架构的配置的aop:declare-partents,或者如果使用基于注解的实现,则使用@DeclareParents。

使用架构添加引入时,aop:declare-parent为被建议的 bean 声明一个新的父级。配置如下:

<aop:aspect> 
  <aop:declare-parents types-matching="" implement-interface=" 
    default-impl="" /> 
</aop:aspect> 

其中,

  • 类型匹配 - 指定被建议的 been 的匹配类型

  • 实现 - 接口 - newly introduced interface

  • 默认实现 - 实现新引入接口的类

在使用注解的情况下,开发者可以使用@DeclareParents,它相当于aop:declare-parents配置。@DeclareParents 将应用于新引入的接口的属性。@DeclareParents 的语法如下所示:

@DeclareParents(value=" " , defaultImpl=" ") 

在哪里,

  • value - 指定要与接口引入的 bean

  • defaultImpl - 与aop:declare-parent属性中的 default-impl 等效,它指定了提供接口实现的类。

让我们在 JdbcTemplate 应用程序中使用介绍。BookDAO 没有获取书籍描述的方法,所以让我们添加一个。我们将使用 Ch03_JdbcTemplate 作为基础应用程序。按照以下步骤使用介绍:

  1. 创建一个新的 Java 应用程序,并将其命名为 Ch04_Introduction。

  2. 添加所有 Spring 核心、Spring -jdbc、Spring AOP 所需的 jar,正如早期应用程序中所做的那样。

  3. 复制 com.packt.ch03.beans 包。

  4. 创建或复制 com.packt.ch03.dao,带有 BookDAO.java 和 BookDAO_JdbcTemplate.java 类。

  5. 将 connection_new.xml 复制到类路径中,并删除 id 为'namedTemplate'的 bean。

  6. 在 com.packt.ch03.dao 包中创建新的接口 BookDAO_new,如下所示,以声明 getDescription()方法:

      public interface BookDAO_new { 
        String getDescription(long ISBN); 
      }
  1. 创建实现 BookDAO_new 接口的类 BookDAO_new_Impl,它将使用 JdbcTemplate 处理 JDBC。代码如下所示:
      @Repository 
      public class BookDAO_new_Impl implements BookDAO_new { 
        @Autowired 
        JdbcTemplate jdbcTemplate; 
        @Override 
        public String getDescription(long ISBN) { 
          // TODO Auto-generated method stub 
          String GET_DESCRIPTION=" select description from book where           ISBN=?"; 
          String description=jdbcTemplate.queryForObject(
            GET_DESCRIPTION, new Object[]{ISBN},String.class);
          return description; 
        }
      }
  1. 在 com.packt.ch04.aspects 包中创建一个方面类 MyIntroductionAspect,它将向使用 getDescription()方法的新接口介绍。代码如下所示:
      @Aspect 
      public class MyIntroductionAspect { 
        @DeclareParents(value="com.packt.ch03.dao.BookDAO+",
        defaultImpl=com.packt.ch03.dao.BookDAO_new_Impl.class)
        BookDAO_new bookDAO_new; 
      }

注解提供了 BookDAO_new 的介绍,它比 BookDAO 接口中可用的方法多。要用于介绍的默认实现是 BookDAO-new_Impl。

  1. 在 connection_new.xml 中注册方面,如下:
      <bean class="com.packt.ch04.aspects.MyIntroductionAspect"></bean>
  1. 添加以下配置以启用自动代理,
      <aop:aspectj-autoproxy proxy-target-class="true"/>

代理目标类用于强制代理成为我们类的子类。

  1. 复制或创建 MainBookDAO_operation.java 以测试代码。使用 getDescription()方法查找代码描述。以下代码中的下划线语句是需要添加的额外语句:
      public class MainBookDAO_operations { 
        public static void main(String[] args) { 
          // TODO Auto-generated method stub 
          ApplicationContext context=new  
            ClassPathXmlApplicationContext("connection_new.xml"); 
          BookDAO bookDAO=(BookDAO)  
            context.getBean("bookDAO_jdbcTemplate"); 
          //add book
          int rows=bookDAO.addBook(new Book("Java EE 7 Developer  
          Handbook", 97815674L,"PacktPub
          publication",332,"explore the Java EE7
          programming","Peter pilgrim"));
          if(rows>0) 
          { 
            System.out.println("book inserted successfully"); 
          } 
          else
            System.out.println("SORRY!cannot add book"); 

          //update the book
          rows=bookDAO.updateBook(97815674L,432); 
          if(rows>0) 
          { 
            System.out.println("book updated successfully"); 
          }else 
          System.out.println("SORRY!cannot update book"); 
          String desc=((BookDAO_new)bookDAO).getDescription(97815674L); 
          System.out.println(desc); 

          //delete the book
          boolean deleted=bookDAO.deleteBook(97815674L); 
          if(deleted) 
          { 
            System.out.println("book deleted successfully"); 
          }else 
          System.out.println("SORRY!cannot delete book"); 
        } 
      } 

由于 BookDAO 没有 getDescription()方法,为了使用它,我们需要将获得的对象转换为 BookDAO_new。

  1. 执行后,我们将在控制台获得以下输出:
      book inserted successfully 
      book updated successfully 
      explore the Java EE7 programming 
      book deleted successfully 

输出清楚地显示,尽管我们没有改变 BookDAO 及其实现,就能引入 getDescription()方法。

第五章.保持一致性:事务管理

在上一章中,我们深入讨论了使用日志机制作为交叉技术的面向方面编程。事务管理是另一种交叉技术,在处理持久性时在应用程序中扮演着非常重要的角色。在本章中,我们将通过讨论以下几点来探索事务管理:

  • 事务管理是什么?

  • 事务管理的重要性。

  • 事务管理的类型

  • Spring 和事务管理

  • Spring 框架中基于注解的事务管理

许多开发者经常谈论这个花哨的术语“事务管理”。我们中有多少人觉得自己在使用它或其自定义时感到舒适呢?它真的那么难以理解吗?在代码中添加事务是否需要添加大量的复杂代码?不是的!!实际上,它是最容易理解的事情之一,也是最容易开发的。在讨论、设计、开发与数据库进行数据处理的“持久层”时,事务管理非常普遍。事务是序列化多个数据库操作的基本单位,其中要么所有操作成功执行,要么一个都不执行。事务管理是处理事务的技术,通过管理其参数来处理事务。事务根据给定的事务参数保持数据库的一致性,以便要么事务单位成功,要么失败。事务绝不可能部分成功或失败。

现在你可能在想,如果其中的任何一个失败了会有什么大不了的?为什么这如此重要?让我们通过一个实际的场景来理解交易。我们想在某个网上购物网站上开设一个账户。我们需要填写一个表格,提供一些个人信息,并选择一个用户名来进行我们的网上购物。这些信息将由应用程序收集,然后保存在两个表中。一个是以用户名为主键的用户表,第二个是 user_info 表,用于存储用户的个人信息。在从用户那里收集数据后,开发者会对 user_info 表执行插入操作,然后将数据插入到用户表中。现在考虑这样一个场景:从用户那里收集的数据成功插入到 user_info 表中,但不幸的是,用户名在表中已经存在,所以第二个操作失败了。数据库处于不一致的状态。从逻辑上讲,数据应该要么同时添加到两个表中,要么一个都不添加。但在我们的案例中,数据只插入了一个表,而没有插入第二个表。这是因为我们在检查行是否插入成功之前就执行了永久的插入操作,现在即使第二个操作失败了也无法撤销。事务管理帮助开发者通过在数据库表中正确反映所有操作,或者一个都不反映来维护数据库的一致性和完整性。如果在单元操作中任何一个操作失败,所有在失败之前所做的更改都将被取消。当然,这不会自动发生,但开发者需要发挥关键作用。在 JDBC 中,开发者选择不使用自动提交操作,而是选择提交事务或回滚,如果其中任何一个操作失败。这两个术语在事务管理中非常重要。提交将更改永久反映到数据库中。回滚撤销所有在失败发生之前的操作所做的更改,使数据库恢复到原始状态。

以下是 Jim Gray 在 1970 年代定义的 ACID 属性,用于描述事务。这些属性后来被称为 ACID 属性。Gray 还描述了实现 ACID 属性的方法。让我们逐一讨论它们:

  • 原子性:在数据库上连续执行多个操作时,要么所有操作都会成功执行,要么一个都不会执行。开发者可以控制是否通过提交它们来永久更改数据库,或者回滚它们。回滚将撤销所有操作所做的更改。一旦数据被提交,它就不能再次回滚。

  • 一致性:为了将数据保存成适当排列且易于维护的格式,在创建数据库表时设置了规则、数据类型、关联和触发器。一致性确保在从一种状态转换到另一种状态获取数据时,将保持所有设置在其上的规则不变。

  • 隔离性:在并发中,多个事务同时发生导致数据管理问题。隔离性通过锁定机制保持数据的一致状态。除非正在处理数据的事务完成,否则它将保持锁定。一旦事务完成其操作,另一个事务将被允许使用数据。

以下是 ANSI 或 ISO 标准定义的隔离级别:

  • 脏读:考虑两个事务 A 和 B 正在运行的数据集。事务 A 进行了某些更改但尚未提交。与此同时,事务 B 读取了数据以及未提交更改的数据。如果事务 A 成功完成其操作,两个事务具有相同的数据状态。但如果事务 A 失败,它所做的数据更改将被回滚。由于 B 读取了未提交的数据,A 和 B 的数据集将不同。事务 B 使用了过时的数据,导致应用程序的业务逻辑失败。

  • 非可重复读:再次考虑事务 A 和 B 正在完成一些操作。它们都读取了数据,事务 A 更改了一些值并成功提交。事务 B 仍在处理旧数据,导致不良影响。这种情况可以通过在第一个事务完成之前保持数据锁定来避免。

  • 幻读:事务 A 和 B 拥有同一组数据。假设事务 A 已经执行了搜索操作,比如,A 根据书名搜索数据。数据库返回了 8 行数据给事务 A。此时事务 B 在表中插入了一行具有与 A 搜索的书名相同值的数据。实际上表中有 9 行数据,但 A 只得到了 8 行。

  • 可串行化:这是最高的隔离级别,它锁定选定的使用数据,以避免幻读问题。

  • 以下是数据库支持的默认隔离级别:

数据库 默认隔离级别
Oracle READ_COMMITTED
Microsoft SQL Server READ_COMMITTED
MySQL REPEATABLE_READ
PostgreSQL READ_COMMITTED
DB2 CURSOR STABILITY
  • 持久性:事务通过多种操作同时进行更改。持久性指定一旦数据库中的数据被更改、添加或更新,它必须是永久的。

一旦我们了解了描述事务的属性,了解事务进展的阶段将有助于我们有效地使用事务管理。

事务管理的生命周期


以下图表展示了每个事务进展的阶段:

新启动的事务将经历以下阶段:

  1. 活动:事务刚刚开始并且正在向前推进。

  2. 部分提交:一旦操作成功执行,生成的值将被存储在易失性存储中。

  3. 失败:在失败之前生成的值不再需要,将通过回滚从易失性存储区中删除它们。

  4. 中止:操作已失败且不再继续。它将被停止或中止。

  5. 已提交:所有成功执行的操作以及操作期间生成的所有临时值,一旦事务提交,将被永久存储。

  6. 终止:当事务提交或中止时,它达到了其最终阶段——终止。

要处理与生命周期步骤和属性相关的事务,不能忽视一个非常重要的事实,即了解事务的类型。事务可以划分为本地事务或全局事务。

本地事务

本地事务允许应用程序连接到单个数据库,一旦事务中的所有操作成功完成,它将被提交。本地事务特定于资源,不需要服务器处理它们。配置的数据源对象将返回连接对象。此连接对象进一步允许开发人员根据需要执行数据库操作。默认情况下,此类连接是自动提交的。为了掌握控制权,开发人员可以使用提交或回滚手动处理事务。JDBC 连接是本地事务的最佳示例。

全局或分布式事务

全局事务由应用服务器如 Weblogic、WebSphere 管理。全局事务能够处理多个资源和服务器。全局事务由许多访问资源的本地事务组成。EJB 的容器管理事务使用全局事务。

Spring 和事务管理

Spring 框架卓越地支持事务管理器的集成。它支持 Java 事务 API,JDBC,Hibernate 和 Java 持久化 API。框架支持称为事务策略的抽象事务管理。事务策略是通过服务提供者接口(SPI)通过 PlatformTransactionManager 接口定义的。该接口有提交和回滚事务的方法。它还有获取由 TransactionDefinition 指定的事务的方法。所有这些方法都会抛出 TransactionException,这是一个运行时异常。

getTransaction()方法根据 TransactionDefinition 参数返回 TransactionStatus。该方法返回的 TransactionStatus 代表一个新事务或现有事务。以下参数可以指定以定义 TransactionDefinition:

  • 传播行为:当一个事务方法调用另一个方法时,传播行为就会讨论。在这种情况下,传播行为指明它将如何执行事务行为。调用方法可能已经启动了事务,那么被调用方法在这种情况下应该做什么?被调用方法是启动一个新事务,使用当前事务还是不支持事务?传播行为可以通过以下值来指定:

    • REQUIRED:它表示必须有事务。如果没有事务存在,它将创建一个新的事务。

    • REQUIRES_NEW:它指定每次都要有一个新的事务。当前事务将被挂起。如果没有事务存在,它将创建一个新的事务。

    • 强制:它表示当前事务将被支持,但如果没有进行中的事务,将抛出异常。

    • 嵌套:它表明,如果当前事务存在,方法将在嵌套事务中执行。如果没有事务存在,它将作为 PROPAGATION_REQUIRED 行事。

    • 永不:不支持事务,如果存在,将抛出异常。

    • 不支持:它表示该交易是不被支持的。如果交易与永不相反存在,它不会抛出异常,但会挂起交易。

  • 隔离性:我们已经在深度讨论隔离级别。

  • 超时:事务中提到的超时值,以秒为单位。

  • 只读:该属性表示事务将只允许读取数据,不支持导致更新数据的操作。

以下是用 Spring 框架进行事务管理的优点:

Spring 通过以下两种方式简化事务管理:

  • 编程事务管理。

  • 声明式事务管理。

无论我们使用程序化事务还是声明式事务,最重要的是使用依赖注入(DI)定义PlatformTransactionManager。一个人应该清楚地知道是使用本地事务还是全局事务,因为定义PlatformTransactionManager是必不可少的。以下是一些可以用来定义PlatformTransactionManager的配置:

  • 使用 DataSource PlatformTransactionManager 可以定义为:
      <bean id="dataSource" 
        <!-DataSource configuration --> 
      </bean> 
      <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> 
        <property name="dataSource" ref="dataSource"/>    
      </bean> 

  • 使用 JNDI 和 JTA 定义 PlatformTransactionManager,如下所示:
      <jee: jndi-lookup id="dataSource' jndi-name="jdbc/books"/> 
        <bean id="transactionManager"
          class="org.springframework.transaction.jta.JtaTransactionManager"> 
        </bean> 

  • 使用 HibernateTransactionManager 定义 PlatformTransactionManager 为:
      <bean id="sessionFactory" 
         class="org.springframework.orm.hibernate5.LocalSessionfactoryBean" 
         <!-define parameters for session factory --> 
      </bean> 

      <bean id=" transactionManager"
         class="org.springframework.orm.hibernate5.HibernateTransactionManager"> 
         <property name="sessionFactory" ref="sessionFactory"/> 
      </bean> 

让我们逐一开始使用 Spring 中的事务管理,

程序化事务管理

在 Spring 中,可以通过使用 TransactionTemplate 或 PlatformTransactionManager 来实现程序化事务管理。

使用 PlatformTransactionManager

PlatformTransactionManager 是讨论 Spring 事务管理 API 的中心。它具有提交、回滚的功能。它还提供了一个返回当前活动事务的方法。由于它是一个接口,因此可以在需要时轻松模拟或垫片。Spring 提供了 DataSourceTransactionManager、HibernateTransactionManager、CciLocalTransactionManager、JtaTransactionManager 和 OC4JJtaTransactionManager 作为 PlatformTransactionManager 的几种实现。要使用 PlatformTransactionManager,可以在 bean 中注入其任何实现以用于事务管理。此外,TransactionDefinition 和 TransactionStatus 对象的可以使用来回滚或提交事务。

在前进之前,我们需要讨论一个非常重要的点。通常,应用程序需求决定是否将事务应用于服务层或 DAO 层。但是,是否将事务应用于 DAO 层或服务层仍然是一个有争议的问题。尽管将事务应用于 DAO 层可以使事务更短,但最大的问题将是多事务的发生。并且必须非常小心地处理并发,不必要的复杂性会增加。当将事务应用于服务层时,DAO 将使用单个事务。在我们的应用程序中,我们将事务应用于服务层。

为了在应用程序中应用事务管理,我们可以考虑以下几点,

  • 是将事务应用于 DAO 层还是服务层?

  • 决定是使用声明式事务还是程序化事务管理

  • 在 bean 配置中定义要使用的 PlatformtransactionManager。

  • 决定事务属性,如传播行为、隔离级别、只读、超时等,以定义事务。

  • 根据程序化或声明式事务管理,在代码中添加事务属性。

让我们使用事务来更好地理解。我们将使用第三章中开发的Ch03_JdbcTemplate应用程序作为基础应用程序,并使用PlatformTransactionManager遵循步骤来使用事务,

  1. 创建一个名为Ch05_PlatformTransactionManager的新 Java 应用程序,并添加所有必需的 jar 文件,包括 Spring 核心、Spring-jdbc、Spring-transaction、Spring-aop、commons-logging 和 mysql-connector。

  2. com.packt.ch03.beans包中复制或创建Book.java文件。

  3. com.packt.ch03.dao包中复制或创建BookDAO.javaBookDAO_JdbcTemplate.java文件。应用程序的最终结构如下所示:

  1. 我们将在BookDAO中添加一个新的方法来搜索书籍,因为在添加之前,我们需要找出'Book'表中是否有具有相同 ISBN 的书籍。如果已经存在,我们不希望不必要的再次进行添加。新添加的方法将如下所示:
      public Book serachBook(long ISBN); 

  1. BookDAO_JdbcTemplate.java需要覆盖接口中 newly added method,如下所示:
      @Override 
      public Book serachBook(long ISBN) { 
        // TODO Auto-generated method stub 
        String SEARCH_BOOK = "select * from book where ISBN=?"; 
        Book book_serached = null; 
        try { 
          book_serached = jdbcTemplate.queryForObject(SEARCH_BOOK,  
            new Object[] { ISBN },  
            new RowMapper<Book>(){ 
            @Override 
              public Book mapRow(ResultSet set, int rowNum)  
              throws SQLException { 
                Book book = new Book(); 
                book.setBookName(set.getString("bookName")); 
                book.setAuthor(set.getString("author")); 
                book.setDescription(set.getString("description")); 
                book.setISBN(set.getLong("ISBN")); 
                book.setPrice(set.getInt("price")); 
                book.setPublication(set.getString("publication")); 
                return book; 
              } 
            }); 
            return book_serached; 
          } catch (EmptyResultDataAccessException ex) { 
          return new Book(); 
        } 
      } 

我们添加了一个匿名内部类,它实现了RowMapper接口,使用queryForObject()方法将从数据库检索的对象绑定到Book对象的数据成员。代码正在搜索书籍,然后将ResultSet中的列值绑定到Book对象。我们返回了一个具有默认值的对象,仅为我们的业务逻辑。

  1. com.packt.ch05.service包中添加BookService接口作为服务层,并具有以下方法签名:
      public interface BookService { 
        public Book searchBook(long ISBN); 
        public boolean addBook(Book book); 
        public boolean updateBook(long ISBN, int price); 
        public boolean deleteBook(long ISBN); 
      } 

  1. 创建BookServiceImpl实现BookService。因为这是服务,用@Service注解类。

  2. 首先向类中添加两个数据成员,第一个类型为PlatformTransactionManager以处理事务,第二个类型为BookDAO以执行 JDBC 操作。使用@Autowired注解对它们进行依赖注入。

  3. 首先,让我们分两步为服务层开发searchBook()方法,以处理只读事务:

    • 创建一个TransactionDefinition实例。

    • 创建一个TransactionStatus实例,该实例从使用上一步创建的TransactionDefinition实例的TransactionManager中获取。TransactionStatus将提供事务的状态信息,该信息将用于提交或回滚事务。

在这里,将事务设置为只读,将属性设置为true,因为我们只是想要搜索书籍,并不需要在数据库端执行任何更新。至此步骤开发出的代码将如下所示:

      @Service(value = "bookService") 
      public class BookServiceImpl implements BookService { 
        @Autowired 
        PlatformTransactionManager transactionManager; 

        @Autowired  
        BookDAO bookDAO; 

        @Override 
        public Book searchBook(long ISBN) { 
          TransactionDefinition definition = new  
            DefaultTransactionDefinition(); 
          TransactionStatus transactionStatus =  
            transactionManager.getTransaction(definition); 
          //set transaction as read-only 
          ((DefaultTransactionDefinition)  
          definition).setReadOnly(true); 
          Book book = bookDAO.serachBook(ISBN); 
          return book; 
        } 
        // other methods from BookService     
      }   

我们更新只读事务属性的方式,也可以同样设置其他属性,如隔离级别、传播、超时。

  1. 让我们向服务层添加addBook()方法,以找出是否已有具有相同 ISBN 的书籍,如果没有,则在表中插入一行。代码将如下所示:
      @Override 
      public boolean addBook(Book book) { 
        // TODO Auto-generated method stub 
        TransactionDefinition definition = new  
          DefaultTransactionDefinition(); 
        TransactionStatus transactionStatus =  
          transactionManager.getTransaction(definition); 

        if (searchBook(book.getISBN()).getISBN() == 98564567l) { 
          System.out.println("no book"); 
          int rows = bookDAO.addBook(book); 
          if (rows > 0) { 
            transactionManager.commit(transactionStatus); 
            return true; 
          } 
        } 
        return false; 
      } 

transactionManager.commit()将永久将数据提交到书籍表中。

  1. 以同样的方式,让我们添加deleteBookupdateBook()方法,如下所示,
      @Override 
      public boolean updateBook(long ISBN, int price) { 
        TransactionDefinition definition = new  
          DefaultTransactionDefinition(); 
        TransactionStatus transactionStatus =  
          transactionManager.getTransaction(definition); 
        if (searchBook(ISBN).getISBN() == ISBN) { 
          int rows = bookDAO.updateBook(ISBN, price); 
          if (rows > 0) { 
            transactionManager.commit(transactionStatus); 
            return true; 
          } 
        } 
        return false; 
      } 

      @Override 
      public boolean deleteBook(long ISBN)  
      { 
        TransactionDefinition definition = new  
          DefaultTransactionDefinition(); 
        TransactionStatus transactionStatus =  
          transactionManager.getTransaction(definition); 
        if (searchBook(ISBN).getISBN() != 98564567l) { 
          boolean deleted = bookDAO.deleteBook(ISBN); 
          if (deleted) { 
            transactionManager.commit(transactionStatus); 
            return true; 
          } 
        } 
        return false; 
      } 

  1. 复制或创建 connection_new.xml 以进行 bean 配置。添加一个 DataSourceTransactionManager 的 bean,正如我们在讨论如何使用 DataSource 配置 PlatformTransactionManager 时所看到的。

  2. 更新从 XML 中扫描包,因为我们还想考虑新添加的包。更新后的配置如下:

      <context:component-scan base- package="com.packt.*">
      </context:component-scan> 

  1. 最后一步将是把主代码添加到 MainBookService_operation.java 中,该文件将使用 BookServiceImpl 对象调用服务层的方法,就像我们之前对 BookDAO_JdbcTemplate 对象所做的那样。代码如下所示:
      public static void main(String[] args) { 
        // TODO Auto-generated method stub 
        ApplicationContext context = new   
          ClassPathXmlApplicationContext("connection_new.xml"); 
        BookService service = (BookService)    
          context.getBean("bookService"); 
        // add book 
        boolean added = service.addBook(new Book("Java EE 7  
          Developer Handbook", 97815674L, "PacktPub  
          publication", 332,  "explore the Java EE7  
          programming", "Peter pilgrim")); 
        if (added) { 
          System.out.println("book inserted successfully"); 
        } else 
        System.out.println("SORRY!cannot add book"); 
        // update the book 
        boolean updated = service.updateBook(97815674L, 800); 
        if (updated) { 
          System.out.println("book updated successfully"); 
        } else 
        System.out.println("SORRY!cannot update book"); 
        // delete the book 
        boolean deleted = service.deleteBook(97815674L); 
        if (deleted) { 
          System.out.println("book deleted successfully"); 
        } else 
        System.out.println("SORRY!cannot delete book"); 
      } 

TransactionTemplate

使用线程安全的 TransactionTemplate 可以帮助开发者摆脱重复的代码,正如我们已经讨论过的 JdbcTemplate。它通过回调方法使程序化事务管理变得简单而强大。使用 TransactionTemplate 变得容易,因为它有各种事务属性的不同设置方法,如隔离级别、传播行为等。使用 Transaction 模板的第一步是通过提供事务管理器来获取其实例。第二步将是获取 TransactionCallback 的实例,该实例将传递给 execute 方法。以下示例将演示如何使用模板,我们不需要像早期应用程序中那样创建 TransactionDefinition,

  1. 创建一个名为 Ch05_TransactionTemplate 的 Java 应用程序,并复制早期应用程序中所需的所有 jar 文件。

  2. 我们将保持应用程序的结构与 Ch05_PlatformTransactionManager 应用程序相同,因此您可以复制 bean、dao 和服务包。我们唯一要做的改变是在 BookServiceImpl 中使用 TransactionTemplate 而不是 PlatformTransactionManager。

  3. 从 BookServiceImpl 中删除 PlatformTransactionManager 数据成员并添加 TransactionTemplate。

  4. 使用@Autowired 注解来使用 DI。

  5. 我们将更新 searchBook()方法,使其使用 TransactionTemplate,并通过 setReadOnly(true)将其设置为只读事务。TransactionTemplate 有一个名为'execute()'的回调方法,可以在其中编写业务逻辑。该方法期望一个 TransactionCallback 的实例,并返回搜索到的书籍。代码如下所示:

      @Service(value = "bookService") 
      public class BookServiceImpl implements BookService { 
        @Autowired 
        TransactionTemplate transactionTemplate; 

        @Autowired 
        BookDAO bookDAO; 

        public Book searchBook(long ISBN) { 
          transactionTemplate.setReadOnly(true);   
          return transactionTemplate.execute(new  
            TransactionCallback<Book>()  
          { 
            @Override 
            public Book doInTransaction(TransactionStatus status) { 
              // TODO Auto-generated method stub 
              Book book = bookDAO.serachBook(ISBN); 
              return book; 
          }     
        });  
      }  

为了执行任务,我们通过内部类的概念创建了 TransactionCallback 的实例。这里指定的泛型类型是 Book,因为它是 searchBook()方法的返回类型。这个类重写了 doInTransaction()方法,以调用 DAO 的 searchBook()方法中的业务逻辑。

还可以再实现一个 TransactionCallback 的版本,使用 TransactionCallbackWithoutResult。这种情况下可以用于服务方法没有返回任何内容,或者其返回类型为 void。

  1. 现在让我们添加addBook()。我们首先必须使用searchBook()查找书籍是否存在于表中。如果书籍不存在,则添加书籍。但由于searchBook()使事务变为只读,我们需要更改行为。由于addBook()有布尔值作为其返回类型,我们将使用布尔类型的TransactionCallBack。代码将如下所示:
      @Override 
      public boolean addBook(Book book) { 
        // TODO Auto-generated method stub 
        if (searchBook(book.getISBN()).getISBN() == 98564567l)  
        { 
          transactionTemplate.setReadOnly(false); 
          return transactionTemplate.execute(new  
            TransactionCallback<Boolean>()  
          { 
            @Override 
            public boolean doInTransaction(TransactionStatus status) { 
              try { 
                int rows = bookDAO.addBook(book); 
                if (rows > 0) 
                  return true; 
              } catch (Exception exception) { 
                status.setRollbackOnly(); 
              } 
              return false; 
            } 
          }); 
        } 
        return false; 
      } 

代码清楚地显示了 TransactionTemplate 赋予我们更改尚未内部管理的事务属性的能力,而无需编写 PlatformTransactionManager 所需的模板代码。

  1. 同样,我们可以为deleteBookupdateBook()添加代码。你可以在线源代码中找到完整的代码。

  2. Ch05_PlatformTransactionmanager类路径中复制connection_new.xml文件,并添加一个TransactionTemplate的 bean,如下所示:

      <bean id="transactionTemplate"
        class="org.springframework.transaction.support.TransactionTemplate"> 
        <property name="transactionManager"  
          ref="transactionManager"></property> 
      </bean> 

我们已经有了一个事务管理器的 bean,所以我们在这里不会再次添加它。

  1. MainBookService_operations.java文件复制到默认包中以测试代码。我们会发现代码成功执行。

  2. 在继续前进之前,请按照如下方式修改searchBook()方法中的doInTransaction()代码;

      public Book doInTransaction(TransactionStatus status) { 
        //Book book = bookDAO.serachBook(ISBN); 
        Book book=new Book(); 
        book.setISBN(ISBN); 
        bookDAO.addBook(book); 
        return book; 
      } 

  1. 执行后,我们会得到一个堆栈跟踪,如下所示,它表示只读操作不允许修改数据:
      Exception in thread "main" 
      org.springframework.dao.TransientDataAccessResourceException:  
      PreparedStatementCallback; SQL [insert into book values(?,?,?,?,?,?)];  
      Connection is read-only. Queries leading to data modification are not
      allowed; nested exception is java.sql.SQLException:
      Connection is read- only.  

声明式事务管理

Spring 框架使用 AOP 来简化声明式事务管理。声明式事务管理最好的地方在于,它不一定需要由应用服务器管理,并且可以应用于任何类。该框架还通过使用 AOP,使开发者能够定制事务行为。声明式事务可以是基于 XML 的,也可以是基于注解的配置。

基于 XML 的声明式事务管理:

该框架提供了回滚规则,用于指定事务将在哪种异常类型下回滚。回滚规则可以在 XML 中如下指定,

<tx:advise id=:transactionAdvise" transaction-manager="transactionamanager">  
  <tx:attributes> 
     <tx:method name="find*" read-only="true" 
       rollback- for ="NoDataFoundException'> 
    </tx:attributes> 
  </tx:advise> 

配置甚至可以指定属性,例如,

  • 'no-rollback-for' - 用以指定我们不想回滚事务的异常。

  • 传播 - 用以指定事务的传播行为,其默认值为'REQUIRED'。

  • 隔离 - 用以指定隔离级别。

  • 超时 - 以秒为单位的事务超时值,默认值为'-1'。

由于现在我们更倾向于使用注解事务管理,而不浪费时间,让我们继续讨论注解事务管理。

基于注解的事务管理

@Transaction注解有助于开发基于注解的声明式事务管理,它可以应用于接口级别、类级别以及方法级别。要启用注解支持,需要配置以下配置以及事务管理器,

<bean id="transactionManager" class=" your choice of transaction manager"> 
  <!-transaction manager configuration - -> 
</bean> 
<tx:annotation-driven transaction-manager="transcationManager"/> 

如果为 PlatformTransactionManager 编写的 bean 名称是'transactionManager',则可以省略'transaction-manager'属性。

以下是可以用来自定义事务行为的属性,

  • - 用于指定要使用的事务管理器。

  • 传播行为 - 用于指定传播行为。

  • 隔离级别 - 用于指定隔离级别。

  • 只读 - 用于指定读或写行为。

  • 超时 - 用于指定事务超时。

  • rollbackForClassName - 用于指定导致事务回滚的异常类数组。

  • rollbackFor - 用于指定导致事务回滚的异常类数组。

  • noRollbackFor - 用于指定不导致事务回滚的异常类数组。

  • noRollbackForClassName - 用于指定不导致事务回滚的异常类数组。

让我们使用@Transactional 来演示应用程序中的声明式事务管理,而不是使用以下步骤的帮助进行程序化事务管理:

  1. 创建 Ch05_Declarative_Transaction_Management 并添加所需的 jar,就像在早期的应用程序中一样。

  2. 从 Ch05_PlatformTransactionManager 应用程序中复制 com.packt.ch03.beans 和 com.packt.ch03.dao。

  3. 在 com.packt.ch05.service 包中复制 BookService.java 接口。

  4. 在 com.packt.ch05.service 包中创建 BookServiceImpl 类,并添加一个类型为 BookDAO 的数据成员。

  5. 用@Autowired 注解类型为 BookDAO 的数据成员。

  6. 用@Transactional(readOnly=true)注解 searchBook(),并编写使用 JdbcTemplate 搜索数据的代码。类如下:

      @Service(value = "bookService") 
      public class BookServiceImpl implements BookService { 

        @Autowired 
        BookDAO bookDAO; 

        @Override 
        @Transactional(readOnly=true) 
        public Book searchBook(long ISBN)  
        { 
          Book book = bookDAO.serachBook(ISBN); 
          return book; 
        } 

  1. 从 classpath 中的 Ch05_PlatformTransactionManager 复制 connection_new.xml。

  2. 现在,我们需要告诉 Spring 找到所有被@Trasnactional 注解的 bean。通过在 XML 中添加以下配置即可简单完成:

      <tx:annotation-driven /> 

  1. 要添加上述配置,我们首先要在 XML 中添加'tx'作为命名空间。从 connection_new.xml 更新模式配置如下:
      <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" 
        xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.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.xsd"> 

  1. 现在,我们可以添加以下配置:
      <tx:annotation-driven /> 

  1. 复制 MainBookService_operation.java 并执行它以获得输出。

  2. 现在添加 addBook()方法以理解 readOnly=true。代码如下:

      @Transactional(readOnly=true) 
      public boolean addBook(Book book) { 
        if (searchBook(book.getISBN()).getISBN() == 98564567l) { 
          System.out.println("no book"); 
          int rows = bookDAO.addBook(book); 

          if (rows > 0) { 
            return true; 
          } 
        } 
        return false;  
      } 

  1. 执行 MainBookService_operation.java,并执行它以获得以下输出,指定不允许读取事务修改数据:
      Exception in thread "main" 
      org.springframework.dao.TransientDataAccessResourceException:  
      PreparedStatementCallback; SQL [insert into book values(?,?,?,?,?,?)];
      Connection is read-only. Queries leading to data modification are not
      allowed; nested exception is java.sql.SQLException:Connection is read-only.
      Queries leading to data modification are not allowed 

  1. 编辑 addBook()方法,通过指定 read-only=false 来移除只读事务,这是事务的默认行为。

  2. 主要代码将成功执行操作。

注意

如果应用程序具有少量事务操作,可以使用 TransactionTemplate 进行程序化事务管理。在拥有大量事务操作的情况下,为了保持简单和一致,选择声明式事务管理。

总结


在这一章中,我们讨论了事务以及为什么它很重要。我们还讨论了事务管理及其生命周期。我们讨论了事务属性,如只读、隔离级别、传播行为和超时。我们看到了声明性和编程性作为处理事务的两种方式,其中一种使另一种摆脱了管道代码,而另一种提供了对操作的精细控制。我们还通过一个应用程序讨论了这两种技术,以更好地理解。到目前为止,我们讨论了如何处理想象中的数据。我们需要一种方法将其实际地提供给用户。

在下一章中,我们将探索如何开发一个应用程序的 Web 层,以便让我们进行用户交互。

第六章。探索 Spring MVC

到目前为止,我们已经讨论了如何使用 Spring 框架来处理、初始化和使用数据,同时将控制台作为我们的输出。我们还没有在呈现或用户交互方面付出任何努力。在当今世界,使用老式的基于窗口的、非常单调的呈现方式工作似乎非常无聊。我们希望有更有趣、更令人兴奋的东西。互联网是使世界比以往任何时候都更加紧密和有趣的“东西”。当今的世界是网络的世界,那么我们如何能与之脱节呢?让我们深入到一个令人惊叹的互联网世界,借助以下几点来探索 Spring 的强大功能:

  • 为什么有必要学习使用 Spring 进行网络应用程序开发?

  • 如何使用 Spring MVC 开发网络应用程序?

  • Spring MVC 的不同组件有哪些?

  • 如何预填充表单并将数据绑定到对象?

  • 我们还将讨论如何在 Spring 中执行验证。

在 20 世纪 90 年代,互联网为我们打开了一个完全新世界的大门。这是一个前所未有的数据海洋。在互联网之前,数据只能通过硬拷贝获得,主要是书籍和杂志。在早期,互联网只是用来分享静态数据,但随着时间的推移,互联网的维度、意义和用途发生了很大变化。如今,我们无法想象没有互联网的世界。这几乎是不可思议的。它已经成为我们日常生活中的一部分,也是我们业务行业的一个非常重要的来源。对于我们开发者来说,了解网络应用程序、其开发、挑战以及如何克服这些挑战也非常重要。

在 Java 中,可以使用 Servlet 和 JSP 创建基本网络应用程序,但随后发生了许多演变。这些演变主要是由于不断变化的世界对高需求的时间紧迫。不仅是呈现方式,而且整个网络体验也因 HTML5、CSS、JavaScript、AJAX、Jquery 等类似技术的使用而发生了变化。Servlet 处理网络请求并使用请求参数中的数据提取动态网络应用程序的数据。

在使用 Servlet 和 JSP 时,开发者必须付出很多努力来执行数据转换并将数据绑定到对象。除了执行业务逻辑的主要角色外,他们现在还必须处理额外的负担,即处理请求和响应呈现。

开发者主要在 web 应用程序中处理从请求提取的数据。他们根据规则开发复杂、较长的业务逻辑来执行任务。但如果从请求参数中提取的数据不正确,这一切都是徒劳的。这显然不是开发者的错,但他们的业务逻辑仍然受到影响,使用这样的数据值进行业务逻辑是没有意义的。开发者现在需要特别注意,在执行业务逻辑之前,首先要找出从请求中提取的数据是否正确。开发者还必须 extensively 参与数据呈现到响应中。首先,开发者需要将数据绑定到响应中,然后进一步如何在呈现方面提取它。

上述讨论的每一个任务都在有限的时间内给开发方增加了额外的负担。Spring 框架通过以下特性方便开发者进行简单和快速的开发:

  • Spring 框架支持 MVC 架构,实现了模型、视图和控制器的清晰分离。

  • 该框架通过将请求参数绑定到命令对象,为开发者提供了豆子的力量,以便轻松处理数据。

  • 它提供了对请求参数的简单验证,执行验证 either with Validator interface or using annotations. 它还可以支持自定义验证规则。

  • 它提供了如@RequestParam、@RequestHeader 等注解,这些注解使请求数据绑定到方法参数而不涉及 Servlet API。

  • 它支持广泛的视图模板,如 JSTL、Freemarker、Velocity 等。

  • 通过使用 ModelMap 对象,使数据从控制器传输到视图变得容易。

  • 它可以轻松地与其他框架集成,如 Apache Struts2.0、JSF 等。

通常,web 应用程序部署在 web 服务器上。应用程序中的每个资源都与 URL 映射,用户使用这些 URL 来访问资源。Servlet 或 JSP 从请求对象中读取数据,对其执行业务逻辑,然后将响应作为结果返回。我们都知道,在任何 web 应用程序中,都会发生这种一般的流程。在这个流程中,最重要的是这些 web 应用程序没有任何 Servlet 或控制器来管理整个应用程序的流程。是的,第一个到达者缺席了。整个应用程序及其流程必须由开发方维护。这就是 Servlet 和 Spring 之间的主要区别所在。


Spring 框架采用 MVC 设计模式,提供了一个前端控制器,处理或获取应用程序接收到的每个请求。以下图表显示了 Spring MVC 如何处理请求以及所有组件都是 Spring MVC 的一部分:

以下步骤为我们提供了 Spring MVC 网络应用程序流程的方向:

  • 每个传入请求首先会击中应用程序的心脏——前端控制器。前端控制器将请求分发到处理器,并允许开发者使用框架的不同功能。

  • 前端控制器有自己的 WebApplicationContext,它是从根 WebApplicationContext 继承而来的。根上下文配置的 bean 可以在应用的上下文和 Servlet 实例之间访问和共享。类似于所有 Servlet,前端控制器在第一次请求时进行初始化。

  • 一旦前端控制器初始化完成,它会寻找位于 WEB-INF 文件夹下名为servlet_name-servlet.xml的 XML 文件。该文件包含了 MVC 特定的组件。

  • 这个配置文件默认命名为XXX-servlet.xml,位于 WEB-INF 文件夹下。这个文件包含了 URL 到可以处理传入请求的控制器的映射信息。在 Spring 2.5 之前,映射是发现处理器的必须步骤,现在我们不再需要。我们现在可以直接使用基于注解的控制器。

  • RequestMappingHandlerMapping会搜索所有控制器,查找带有@RequestMapping注解的@Controller。这些处理器可以用来自定义 URL 的搜索方式,通过自定义拦截器、默认处理器、顺序、总是使用完整路径、URL 解码等属性。

  • 在扫描所有用户定义的控制器之后,会根据 URL 映射选择合适的控制器并调用相应的方法。方法的选择是基于 URL 映射和它支持的 HTTP 方法进行的。

  • 在执行控制器方法中编写的业务逻辑后,现在是生成响应的时候了。这与我们通常的 HTTPResponse 不同,它不会直接提供给用户。相反,响应将被发送到前端控制器。在这里,响应包含视图的逻辑名称、模型数据的逻辑名称和实际的数据绑定。通常,ModelAndView实例会被返回给前端控制器。

  • 逻辑视图名在前端控制器中,但它不提供有关实际视图页面的任何信息。在XXX-servlet.xml文件中配置的ViewResolverbean 将作为中介,将视图名称映射到实际页面。框架支持广泛的视图解析器,我们将在稍后讨论它们。

  • 视图解析器帮助获取前端控制器可以作为响应返回的实际视图。前端控制器通过从绑定的模型数据中提取值来渲染它,然后将其返回给用户。

在我们讨论的流程中,我们使用了诸如前端控制器、ModelAndView、ViewResolver、ModelMap 等许多名称。让我们深入讨论这些类。

分发器 Servlet

DispacherServlet在 Spring MVC 应用程序中充当前端控制器,首先接收每个传入请求。它基本上用于处理 HTTP 请求,因为它从HTTPServlet继承而来。它将请求委托给控制器,解决要作为响应返回的视图。以下配置显示了在web.xml(部署描述符)中的调度器映射:

<servlet>
  <servlet-name>books</servlet-name>
    <servlet-class>
      org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>books</servlet-name>
  <url-pattern>*.htm</url-pattern>
</servlet-mapping>

上述配置说明所有以.htm为 URL 模式的请求将由名为books的 Servlet 处理。

有时应用程序需要多个配置文件,其中一些位于根WebApplicationContext中,处理数据库的 bean,一些位于 Servlet 应用程序上下文中,包含在控制器中使用的 bean。以下配置可用于初始化来自多个WebApplicationContext的 bean。以下配置可用于从上下文中加载多个配置文件,例如:

<servlet>
  <servlet-name>books</servlet-name>
    <servlet-class>
      org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>books</servlet-name>
  <url-pattern>*.htm</url-pattern>
</servlet-mapping>

控制器

Spring 控制器用于处理执行业务逻辑的请求,这些控制器也可以被称为'处理器',其方法称为处理器方法。Spring 提供了AbstarctUrlViewControllerParameterizableViewContollerServletForwardingConrollerServletWrappingControllerBefore作为控制器。在 Spring 2.5 基于 web 的应用程序中,需要对这些控制器进行子类化以自定义控制器。但现在,Spring 通过@Controller注解支持注解驱动的控制器。以下配置启用了基于注解的控制器:

<mvc:annotation-driven />

需要发现基于注解的控制器以执行处理器方法。以下配置提供了关于框架应扫描哪些包以发现控制器的信息:

<context:component-scan base-package="com.packt.*">
</context:component-scan>

@RequestMapping注解用于标注类或方法,以声明它能处理的特定 URL。有时同一个 URL 可以注解多个方法,这些方法支持不同的 HTTP 方法。@RequestMapping的'method=RequestMethod.GET'属性用于指定哪个 HTTP 方法将由该方法处理。

ModelAndView

ModelAndView在生成响应中扮演着重要角色。ModelAndView实例使得可以将模型数据绑定到其逻辑名称、逻辑视图名称。在视图中使用的数据对象通常称为模型数据。以下代码段清楚地说明了绑定是如何发生的:

new ModelAndView(logical_name_of_view,logical_name_of_model_data,
  actual_value_of_model_data);

我们甚至可以使用以下代码段:

ModelAndView mv=new ModelAndView();
mv.setViewName("name_of_the_view");
mv.setAttribute(object_to_add);

ModelMap

ModelMap接口是LinkedHashMap的子类,在构建使用键值对的模型数据时使用。它有addAttribute()方法,提供模型和模型逻辑名称的绑定。在ModelMap中设置的属性可以在表单提交时由视图用于表单数据绑定。我们稍后会深入讨论这一点。

视图解析器

用户定义的控制器返回的逻辑视图名称和其他详细信息。视图名称是一个需要由 ViewResolver 解析的字符串。

以下是一些可以用于渲染视图的 ViewResolvers:

  • XmlViewResolver:XmlViewResolver 用于查看编写为 XML 的文件。它使用位于 WEB-INF/views.xml 的默认配置,该配置文件包含与 Spring beans 配置文件相同的 DTD 的视图 bean。配置可以如下所示编写:
      <bean id="myHome"  
        class="org.springframework.web.servlet.view.JstlView"> 
        <property name="url" value="WEB-INF/jsp/home.jsp"/> 
      <bean> 

  • 逻辑视图名 'myHome' 被映射到实际的视图 'WEB-INF/jsp/home.jsp'。

  • 一个 bean 也可以引用映射到另一个 bean 的视图,如下所示:

      <bean id="logout"  
        class="org.springframework.web.servlet.view.RenderView"> 
        <property name="url" value="myHome"/> 
      <bean> 

  • 'logout' bean 没有映射到任何实际的视图文件,但它使用 'myHome' bean 来提供实际的视图文件。

  • UrlBasedViewResolver: 它将 URL 直接映射到逻辑视图名称。当逻辑名称与视图资源相匹配时,它将被优先考虑。它的前缀和后缀作为其属性,有助于获取带有其位置的实际视图名称。该类无法解析基于当前区域设置的视图。为了启用 URLBasedViewResolver,可以编写以下配置:

      <bean id="viewResolver" 
  class="org.springframework.web.servlet.view.UrlBasedViewResolver"> 
        <property name="viewClass" value=   
          "org.springframework.web.servlet.view.JstlView"/> 
        <property name="prefix" value="WEB-INF/jsp/"/> 
        <property name="suffix" value=".jsp"/> 
      <bean> 

  • JstlView 用于渲染视图页面。在我们的案例中,页面名称和位置是 'prefix+ view_name_from_controller+suffix'。

  • InternalResourceViewResolver: InternalResourceViewresolver 是 UrlBasedViewResolver 的子类,用于解析内部资源,这些资源可以作为视图使用,类似于其父类的前缀和后缀属性。AlwaysInclude、ExposeContextBeansAsAttributes、ExposedContextBeanNames 是该类的几个额外属性,使其比父类更频繁地使用。以下配置与之前示例中配置 UrlBasedViewResolver 的方式类似:

      <bean id="viewResolver" class=  
  "org.springframework.web.servlet.view.InternalResourceViewResolver"> 
        <property name="viewClass" value=                
          "org.springframework.web.servlet.view.JstlView"/> 
        <property name="prefix" value="WEB-INF/jsp/"/> 
        <property name="suffix" value=".jsp"/> 
      <bean> 

  • 它只能在到达页面时验证页面的存在,在此之前不会进行验证。

  • ResourceBundleViewResolver: ResourceBundleViewResolver 使用配置中指定的 ResourceBundle 的定义。默认文件用于定义配置的是 views.properties。配置将如下所示:

      <bean id="viewResolver" class= 
  "org.springframework.web.servlet.view.ResourceViewResolver"> 
        <property name="base" value="web_view"/> 
      </bean> 

  • 视图.properties 将指定要使用的视图类的详细信息以及实际视图的 URL 映射,如下所示:
      home.(class)= org.springframework.wev.servlet.view.JstlView 

  • 下面的行指定了名为 homepage 的视图的映射:
       homepage.url= /WEB-INF/page/welcome.jsp 

  • TilesViewResolver: Tiles 框架用于定义可以重用并保持应用程序一致的外观和感觉的页面布局模板。在 'tiles.def' 文件中定义的页面定义作为 tile、header、footer、menus,在运行时组装到页面中。控制器返回的逻辑名称与视图解析器将渲染的 tiles 模板名称匹配。

除了上面讨论的视图解析器之外,Spring 还具有 FreeMarkerViewResolver、TileViewResolver、VelocityLayoutViewResolver、VelocityViewResolver、XsltViewResolver。

在继续讨论之前,让我们首先开发一个示例演示,以详细了解应用程序的流程,并通过以下步骤了解上述讨论的方向:

  1. 创建一个名为 Ch06_Demo_SpringMVC 的动态网页应用程序。

  2. 按照以下项目结构复制 spring-core、spring-context、commons-logging、spring-web 和 spring-webmvc 的 jar 文件:

  1. WebContent文件夹中创建 index.jsp,作为主页。可以根据你的需求自定义名称,就像我们在任何 Servlet 应用程序中所做的那样。

  2. index.jsp中添加一个链接,该链接提供导航到控制器,如下所示:

      <center> 
        <img alt="bookshelf" src="img/img1.png" height="180" 
          width="350"> 
        <br> 
      </center> 
      <a href="welcomeController.htm">Show welcome message</a> 

  1. 每当用户点击链接时,系统会生成一个带有'welcomeCointroller.htm' URL 的请求,该请求将由前端控制器处理。

  2. 是时候在web.xml中配置前端控制器了:

      <servlet> 
        <servlet-name>books</servlet-name> 
        <servlet-class>    
          org.springframework.web.servlet.DispatcherServlet 
        </servlet-class> 
      </servlet> 
      <servlet-mapping> 
        <servlet-name>books</servlet-name> 
        <url-pattern>*.htm</url-pattern> 
      </servlet-mapping> 

  1. 前端控制器查找 WEB-INF 中名为servlet_name-servlet.xml的文件来查找和调用控制器的的方法。在我们的案例中,Servlet 的名称是'books'。所以让我们在 WEB-INF 文件夹下创建一个名为'books-servlet.xml'的文件。

  2. 该文件应包含 Spring 容器将扫描以查找控制器的包的配置。配置将如下所示:

      <context:component-scan base-package=     
        "com.packt.*"></context:component-scan> 

上述配置说明将扫描'com.packt'包中的所有控制器。

  1. 在 com.packt.ch06.controllers 包中创建一个MyMVCController类。

  2. 通过@Controller注解类。注解类使其能够使用处理请求的功能。

  3. 让我们通过如下所示的@RequestMapping注解添加welome()方法来处理请求:

      @Controller 
      public class MyMVCController { 
        @RequestMapping(value="welcomeController.htm") 
        public ModelAndView welcome() 
        { 
          String welcome_message="Welcome to the wonderful  
          world of Books"; 
          return new ModelAndView("welcome","message",welcome_message); 
        } 
      } 

控制器可以有多个方法,这些方法将根据 URL 映射被调用。在这里,我们声明了将被welcomeController.htm' URL 调用的方法。

该方法通过ModelAndView生成欢迎信息并生成响应,如下所示:

      new ModelAndView("welcome","message",welcome_message); 
      The ModelAndView instance is specifying, 
      Logical name of the view -  welcome 
      Logical name of the model data -  message  
      Actual value of the model data - welcome_message 

以上代码的替代方案,你可以使用如下代码:

      ModelAndView mv=new ModelAndView(); 
      mv.setViewName("welcome"); 
      mv.addObject("message", welcome_message); 
      return mv; 

我们可以将多个方法映射到相同的 URL,支持不同的 HTTP 方法,如下所示:

      @RequestMapping(value="welcomeController.htm", 
        method=RequestMethod.GET) 
      public ModelAndView welcome(){ 
        //business logic  
      } 
      @RequestMapping(value="welcomeController.htm", 
        method=RequestMethod.POST) 
      public ModelAndView welcome_new()  { 
        //business logic 
      } 

  1. 如以下所示,在books-servlet.xml中配置ViewResolver bean:
      <bean id="viewResolver" class=
   "org.springframework.web.servlet.view.InternalResourceViewResolver"> 
        <property name="prefix" value="/WEB-INF/jsps/"></property> 
        <property name="suffix" value=".jsp"></property> 
      </bean> 

ViewResolver帮助前端控制器获取实际的视图名称和位置。在前端控制器返回给浏览器的响应页面中,在我们的案例中将是:

  1. 在 WebContent 中创建一个名为 jsps 的文件夹。

  2. 在 jsps 文件夹中创建一个 welcome.jsp 页面,使用表达式语言显示欢迎信息:

      <body> 
        <center> 
          <img alt="bookshelf" src="img/img1.png" height="180" 
            width="350"> 
          <br> 
        </center> 
        <center> 
          <font color="green" size="12"> ${message } </font> 
        </center> 
      </body>

在 EL 中使用属性'message',因为这是我们控制器方法中用于ModelAndView对象逻辑模型名称。

  1. 配置好 tomcat 服务器并运行应用程序。在浏览器中将显示链接。点击链接我们将看到如下截图的输出:

该演示向我们介绍了 Spring MVC 流程。现在让我们逐步开发书籍应用程序,涵盖以下案例:

  • 读取请求参数

  • 处理表单提交

案例 1:读取请求参数

让我们开始通过以下步骤读取请求参数:

  1. 创建 ReadMyBooks 作为动态网络应用程序,并像我们之前那样添加所有必需的 jar 文件。

  2. 每个应用程序都有一个主页。所以,让我们将之前的应用程序中的 index.jsp 作为主页添加进去。您可以直接复制和粘贴。

  3. 从之前的应用程序中复制 images 文件夹。

  4. 在下面所示的位置添加一个链接,用于搜索按作者姓名查找书籍,

      <a href="searchByAuthor.jsp">Search Book By Author</a> 

  1. 让我们添加一个名为 searchByAuthor.jsp 的页面,使用户可以输入作者姓名来请求书籍列表,如下所示:
      <body> 
        <center> 
          <img alt="bookshelf" src="img/img1.png" height="180"  
            width="350"> 
          <br> 

          <h3>Add the Author name to search the book List</h3> 

          <form action="/searchBooks.htm"> 
            Author Name:<input type="text" name="author"> 
            <br>  
            <input  type="submit" value="Search Books"> 
          </form> 
        </center> 
      </body>
  1. 如我们之前所做的那样,在 web.xml 中为 DispachetServlet 作为前端控制器添加配置,并将 servlet 命名为'books'。

  2. 创建或复制 books-servlet.xml,用于从早期应用程序配置处理映射和其他网络组件映射。

  3. 使用'context'命名空间添加扫描控制器的配置。

  4. 我们需要 Book bean 来处理数据往返于控制器。因此,在开发控制器代码之前,请将 Book.java 添加到我们之前应用的 com.packt.ch06.beans 包中,数据成员如下所示:

      public class Book { 
        private String bookName; 
        private long ISBN; 
        private String publication; 
        private int price; 
        private String description; 
        private String author; 
        //getters and setters 
      } 

  1. 现在在 com.packt.ch06.controllers 包中创建一个名为 SearchBookController 的类作为控制器,并用@Controller 注解它。

  2. 为了搜索书籍,需要添加一个名为 searchBookByAuthor()的方法,并用@RequestMapping 注解为'searchBooks.htm'的 URL。我们可以使用 Servlet API 或 Spring API,但在这里我们将使用 Spring API。

  3. 现在让我们为searchBookByAuthor()添加以下代码:

  • 阅读请求参数

  • 搜索书籍列表

  1. 创建 ModelAndView 实例以将书籍列表作为模型数据,逻辑模型名称和逻辑视图名称一起绑定。

代码将如下所示:

      @Controller 
      public class SearchBookController { 
        @RequestMapping(value = "searchBooks.htm") 
        public ModelAndView searchBookByAuthor( 
          @RequestParam("author") String author_name)  
        { 
          // the elements to list generated below will be added by      
          business logic  
          List<Book> books = new ArrayList<Book>(); 
          books.add(new Book("Learning Modular Java Programming",  
            9781235, "packt pub publication", 800, 
            "Explore the Power of Modular Programming ",  
            "T.M.Jog")); 
          books.add(new Book("Learning Modular Java Programming",  
            9781235, "packt pub publication", 800, 
            "Explore the Power of Modular Programming ",   
            "T.M.Jog")); 
          mv.addObject("auth_name",author); 
          return new ModelAndView("display", "book_list", books); 
        } 
      } 

@RequestParam用于读取请求参数并将它绑定到方法参数。'author'属性的值被绑定到 author_name 参数,而不会暴露 servlet API。

在这里,我们添加了一个虚拟列表。稍后,可以将其替换为从持久层获取数据的实际代码。

  1. 是时候在 books-servlet.xml 中配置视图解析器和包扫描,就像我们之前在早期应用程序中做的那样。我们可以将 books-servlet.xml 从早期应用程序的 WEB-INF 中复制粘贴过来。

  2. 在 WebContent 下创建 jsps 文件夹,该文件夹将包含 jsp 页面。

  3. 在 jsps 文件夹中创建 display.jsp,使用 JSTL 标签显示书籍列表,如下所示:

      <%@ taglib prefix="jstl"  
        uri="http://java.sun.com/jsp/jstl/core"%> 
      <html> 
        <head> 
          <meta http-equiv="Content-Type" content="text/html; 
            charset=ISO-8859-1"> 
          <title>Book List</title> 
        </head> 
        <body> 
          <center> 
            <img alt="bookshelf" src="img/img1.png"   
              height="180" width="350"> 
            <br> 
          </center> 
          <jstl:if test="${not empty book_list }"> 
            <h1 align="center"> 
              <font color="green"> 
                Book List of ${auth_name } 
              </font> 
            </h1> 
            <table align="center" border="1"> 
            <tr> 
              <th>Book Name</th> 
              <th>ISBN</th> 
              <th>Publication</th> 
              <th>Price</th> 
              <th>Description</th> 
            </tr> 
            <jstl:forEach var="book_data"  
              items="${book_list}" varStatus="st"> 
              <tr> 
                <td> 
                  <jstl:out value="${ book_data.bookName }"> 
                  </jstl:out> 
                </td> 
                <td> 
                  <jstl:out value="${ book_data.ISBN }"> 
                  </jstl:out> 
                </td> 
                <td> 
                  <jstl:out value="${ book_data.publication }"> 
                  </jstl:out> 
                </td> 
                <td> 
                  <jstl:out value="${ book_data.price }"> 
                  </jstl:out></td> 
                <td> 
                  <jstl:out value="${ book_data.description }"> 
                  </jstl:out> 
                </td> 
              </tr> 
            </jstl:forEach> 
          </table> 
        </jstl:if> 
        <jstl:if test="${empty book_list }"> 
          <jstl:out value="No Book Found"></jstl:out> 
        </jstl:if> 
      </body>

如果列表没有元素,就没有显示该列表的必要。jstl:if 标签用于决定是否显示列表,而 jstl:forEach 标签用于通过迭代列表显示书籍信息。

  1. 运行应用程序,点击主页上的链接以加载表单以输入作者名称。如果作者名称存在,则在表单提交时我们将获得以下书籍列表:

这里,我们使用了@RequestParam将个别请求参数绑定到方法参数。但是,如果请求参数的名称与方法参数的名称匹配,则无需使用注解。更新后的代码可以如下所示:

@RequestMapping(value = "searchBooks.htm") 
public ModelAndView searchBookByAuthor( String author) { 
  List<Book> books = new ArrayList<Book>(); 
  books.add(new Book("Learning Modular Java Programming",  
    9781235, "packt pub publication", 800, 
    "explore the power of modular Programming ",    
    author)); 
  books.add(new Book("Learning Modular Java Programming",  
    9781235, "packt pub publication", 800, 
    "explore the power of modular Programming ",  
    author)); 
  ModelAndView mv= 
    new ModelAndView("display", "book_list", books); 
    mv.addObject("auth_name",author); 
    return mv; 
} 

逐一读取个别请求参数,然后将它们绑定到 bean 对象,变得繁琐而不必要冗长。框架通过处理“表单后盾对象”提供了更好的选项。

情况 2:处理表单提交

表单提交是应用程序开发中非常常见的任务。每次表单提交时,开发者都需要执行以下步骤:

  1. 读取请求参数

  2. 将请求参数值转换为所需数据类型

  3. 将值设置到 bean 对象中。

上述步骤可以省略,直接在表单提交时获取 bean 实例。我们将讨论两种情况的表单处理:

  • 表单提交

  • 表单预处理

表单提交

在普通的网络应用程序中,用户点击一个链接后,表单会被加载,然后手动执行上述讨论的步骤。由于需要自动化这个过程,而不是直接显示表单,因此应该从控制器加载表单,而该控制器已经有一个 bean 实例。在表单提交时,用户输入的值会被绑定到这个实例。现在,这个实例可以在控制器中用于执行业务逻辑。从 Spring 2.0 开始,提供了一组标签,这些标签在视图中处理表单绑定,从而使开发变得容易。

让我们在 ReadMyBooks 应用程序中添加一个表单,以了解使用 Spring 提供的表单标签进行表单提交。我们将分两步进行,第一步显示表单,第二步处理提交的表单。

显示表单

由于表单必须从控制器加载,让我们按照以下步骤添加代码,

  1. 在主页上添加一个链接以加载表单。获取表单的代码如下所示:
      <a href="showBookForm.htm">Show Form to add new Book</a> 

  1. AddBookController中添加showBookForm()方法,该方法将在步骤 1 中点击的链接上被调用。该方法将返回一个表单页面,使用 Book 对象,其中输入的数据将被绑定。该方法的代码如下,
      @RequestMapping("/showBookForm.htm") 
      public ModelAndView showBookForm(ModelMap map)  
      throws Exception { 
        Book book=new Book(); 
        map.addAttribute(book); 
        return new ModelAndView("bookForm"); 
      } 

该方法应该有一个ModelMap作为其参数之一,以添加一个 bean 实例,该实例可以被视图使用。在这里,我们添加了'book'属性,其值为 book 实例。默认情况下,引用名将被用作属性名。'book'实例也可以被称为“表单后盾”对象。为了自定义在视图中使用的表单后盾对象的名称,我们可以使用以下代码:

      map.addAttribute("myBook",book); 

  1. 因为视图名称'bookForm'由控制器返回,所以在 jsps 文件夹中添加bookForm.jsp,该文件包含显示表单的表单。

  2. 用户输入的值需要绑定到表单。Spring 框架提供了强大的标签来处理用户输入。为了使 Spring 标签生效,我们需要添加如下所示的'taglib'指令:

      <%@ taglib prefix="form"  
        uri="http://www.springframework.org/tags/form"%> 

  1. Spring 提供了与 html 类似的标签来处理表单、输入、复选框、按钮等,主要区别在于它们的值隐式绑定到 bean 数据成员。以下代码将允许用户输入书籍名称,并在表单提交时将其绑定到 Book bean 的'bookName'数据成员:
      <form:input path="bookName" size="30" /> 

'path'属性将输入值映射到 bean 数据成员。值必须按照数据成员的名称指定。

  1. 让我们在 bookForm.jsp 中添加以下表单,以便用户输入新书籍的值:
      <form:form modelAttribute="book" method="POST"  
        action="addBook.htm"> 
        <h2> 
          <center>Enter the Book Details</center> 
        </h2> 

        <table width="100%" height="150" align="center" border="0"> 
         <tr> 
           <td width="50%" align="right">Name of the Book</td> 
           <td width="50%" align="left"> 
             <form:input path="bookName" size="30" /> 
           </td> 
         </tr> 
         <tr> 
           <td width="50%" align="right">ISBN number</td> 
           <td width="50%" align="left"> 
             <form:input path="ISBN" size="30" /> 
           </td> 
         </tr> 
         <tr> 
           <td width="50%" align="right">Name of the Author</td> 
           <td width="50%" align="left"> 
             <form:input path="author" size="30" /> 
           </td> 
         </tr> 
         <tr> 
           <td width="50%" align="right">Price of the Book</td> 
           <td width="50%" align="left"> 
             <form:select path="price"> 
               <!- 
                 We will add the code to have  
                 predefined values here  
               -->             
             </form:select> 
           </td> 
         </tr> 
         <tr> 
           <td width="50%" align="right">Description of the  
             Book</td> 
           <td width="50%" align="left"> 
             <form:input path="description"  size="30" /> 
           </td> 
         </tr> 
         <tr> 
           <td width="50%" align="right">Publication of the  
             Book</td> 
           <td width="50%" align="left"> 
             <form:input path="publication"  size="30" /> 
           </td> 
         </tr> 
         <tr> 
           <td colspan="2" align="center"><input type="submit"  
             value="Add Book"></td> 
          </tr> 
        </table> 
      </form:form>

属性'modelAttribute'接收由控制器设置的 ModelMap 逻辑属性的值。

  1. 运行应用程序并点击'Show Form to add new book'。

  2. 您将被导航到 bookForm.jsp 页面,在那里您可以输入自己的值。提交后,您将得到 404 错误,因为没有资源被我们编写来处理请求。别担心!!在接下来的步骤中我们将处理表单。

表单后处理
  1. 让我们在 AddController 中添加一个方法,该方法将在表单提交时通过 url 'addBook.htm'调用,如下所示:
      @RequestMapping("/addBook.htm") 
      public ModelAndView addBook(@ModelAttribute("book") Book book) 
      throws Exception { 
          ModelAndView modelAndView = new ModelAndView(); 
          modelAndView.setViewName("display"); 
          //later on the list will be fetched from the table 
          List<Book>books=new ArrayList(); 
          books.add(book); 
          modelAndView.addObject("book_list",books); 
          return modelAndView; 
      } 

当用户提交表单时,他输入的值将被绑定到 bean 数据成员,生成一个 Book bean 的实例。通过@ModelAttribute 注解'book'参数使开发者可以使用绑定值的 bean 实例。现在,无需读取单个参数,进一步获取和设置 Book 实例。

因为我们已经有了 display.jsp 页面来显示书籍,所以我们在这里只是重用它。用户输入的书籍详情稍后可以添加到书籍表中。

  1. 运行应用程序,点击链接获取表单。填写表单并提交以获得以下输出:

输出列表显示了书籍的详细信息,但没有价格,因为价格目前没有设置。我们想要一个带有预定义值的价格列表。让我们继续讨论表单的预处理。

表单预处理

在某些情况下,表单包含一些预定义值,如国家名称或书籍类别的下拉菜单、可供选择的颜色的单选按钮等。这些值可以硬编码,导致频繁更改要显示的值。相反,可以使用常量值,值可以被渲染并在表单中填充。这通常称为表单预处理。预处理可以在两个步骤中完成。

定义要在视图中添加的属性的值

@ModelAttribute用于将模型数据的实例添加到 Model 实例中。每个用@ModelAttribute 注解的方法在其他 Controller 方法之前和执行时都会被调用,并在执行时将模型数据添加到 Spring 模型中。使用该注解的语法如下:

@ModelAttribute("name_of_the_attribute") 
access_specifier return_type name_of_method(argument_list) {  // code   } 

以下代码添加了一个名为'hobbies'的属性,该属性可在视图中使用:

@ModelAttribute("hobbies") 
public List<Hobby> addAttribute() { 
  List<Hobby> hobbies=new ArrayList<Hobby>(); 
  hobbies.add(new Hobby("reading",1)); 
  hobbies.add(new Hobby("swimming",2)); 
  hobbies.add(new Hobby("dancing",3)); 
  hobbies.add(new Hobby("paining",4)); 
  return hobbies; 
} 

Hobby 是一个用户定义的类,其中包含 hobbyName 和 hobbyId 作为数据成员。

在表单中填充属性的值

表单可以使用复选框、下拉菜单或单选按钮向用户显示可用的选项列表。视图中的值可以使用列表、映射或数组为下拉菜单、复选框或单选按钮的值。

标签的一般语法如下所示:

<form:name-of_tag path="name_of_data_memeber_of_bean"  
  items="name_of_attribute" itemLable="value_to display"  
  itemValue="value_it_holds"> 

以下代码可用于使用'hobbies'作为模型属性绑定值到 bean 的 hobby 数据成员,在复选框中显示用户的爱好:

<form:checkboxes path="hobby" items="${hobbies}"    
  itemLabel="hobbyName" itemValue="hobbyId"/>                 

同样,我们可以在运行时为选择标签生成下拉菜单和选项。

注意

当处理字符串值时,可以省略itemLabelitemValue属性。

完整的示例可以参考应用程序Ch06_Form_PrePopulation

让我们更新ReadMyBooks应用程序,在bookForm.jsp中预定义一些价格值,并使用'ModelAttribute'讨论以下步骤中的表单预处理:

  1. 因为表单是由AddController返回到前端控制器,我们想在其中设置预定义的值,因此在addPrices()方法中添加注解。如下所示使用@ModelAttribute注解:
      @ModelAttribute("priceList") 
      public List<Integer> addPrices() { 
        List<Integer> prices=new ArrayList<Integer>(); 
        prices.add(300); 
        prices.add(350); 
        prices.add(400); 
        prices.add(500); 
        prices.add(550); 
        prices.add(600); 
        return prices; 
      } 

上述代码创建了一个名为'pricelist'的属性,该属性可用于视图。

  1. 现在,pricelist属性可以在视图中显示预定义的值。在我们这个案例中,是一个用于添加新书籍的表单,更新bookForm.jsp以显示如下所示的价格列表:
      <form:select path="price"> 
        <form:options items="${priceList}" />   
      </form:select>
  1. 运行应用程序并点击链接,您可以观察到预定义的价格将出现在下拉列表中,如下所示:

用户将在表单中输入值并提交它。

值可以在处理程序方法中获取。但是,我们仍然不能确定只有有效值会被输入并提交。在错误值上执行的业务逻辑总是会失败。此外,用户可能会输入错误数据类型值,导致异常。让我们以电子邮件地址为例。电子邮件地址总是遵循特定的格式,如果格式错误,业务逻辑最终会失败。无论什么情况,我们必须确信只提交有效值,无论是它们的数据类型、范围还是形成。验证正确数据是否会被提交的过程是“表单验证”。表单验证在确保正确数据提交方面起着关键作用。表单验证可以在客户端和服务器端进行。Java Script 用于执行客户端验证,但它可以被禁用。在这种情况下,服务器端验证总是更受欢迎。


Spring 具有灵活的验证机制,可以根据应用程序要求扩展以编写自定义验证器。Spring MVC 框架默认支持在应用程序中添加 JSR303 实现依赖项时的 JSR 303 规范。以下两种方法可用于在 Spring MVC 中验证表单字段,

  • 基于 JSR 303 规范的验证

  • 基于 Spring 的实现,使用 Validator 接口。

基于 Spring Validator 接口的自定义验证器

Spring 提供了 Validator 接口,该接口有一个 validate 方法,在该方法中会检查验证规则。该接口不仅支持 web 层的验证,也可以在任何层使用以验证数据。如果验证规则失败,用户必须通过显示适当的信息性消息来了解这一点。BindingResult 是 Errors 的子类,在执行 validate()方法对模型进行验证时,它持有由 Errors 绑定的验证结果。错误的可绑定消息将使用form:errors标签在视图中显示,以使用户了解它们。

让我们通过以下步骤在我们的 ReadMyBooks 应用程序中添加一个自定义验证器:

  1. 在应用程序的 lib 文件夹中添加 validation-api-1.1.0.final.api.jar 文件。

  2. 在 com.packt.ch06.validators 包中创建 BookValidator 类。

  3. 类实现了 org.springframework.validation.Validator 接口。

  4. 如代码所示,重写 supports()方法,

      public class BookValidator implements Validator { 
        public boolean supports(Class<?> book_class) { 
          return book_class.equals(Book.class); 
        } 
      } 

支持方法确保对象与 validate 方法验证的对象匹配

  1. 现在重写 validate()方法,根据规则检查数据成员。我们将分三步进行:

    1. 设置验证规则

我们将核对以下规则:

  • 书籍名称的长度必须大于 5。

  • 作者的名字必须不为空。

  • 描述必须不为空。

  • 描述的长度必须至少为 10 个字符,最多为 40 个字符。

  • 国际标准书号(ISBN)不应该少于 150。

  • 价格不应该少于 0。

  • 出版物必须不为空。

    1. 编写条件以检查验证规则。

    2. 如果验证失败,使用rejectValue()方法将消息添加到errors实例中

使用上述步骤的方法可以如下所示编写:

      public void validate(Object obj, Errors errors) { 
        // TODO Auto-generated method stub 
        Book book=(Book) obj; 
        if (book.getBookName().length() < 5) { 
          errors.rejectValue("bookName", "bookName.required", 
          "Please Enter the book Name"); 
        } 
        if (book.getAuthor().length() <=0) { 
          errors.rejectValue("author", "authorName.required", 
          "Please Enter Author's Name"); 
        } 
        if (book.getDescription().length() <= 0) 
        { 
          errors.rejectValue("description",  
            "description.required", 
            "Please enter book description"); 
        } 
        else if (book.getDescription().length() < 10 ||  
          book.getDescription().length() <  40) { 
            errors.rejectValue("description", "description.length", 
            Please enter description within 40 charaters only"); 
         } 
         if (book.getISBN()<=150l) { 
           errors.rejectValue("ISBN", "ISBN.required", 
           "Please Enter Correct ISBN number"); 
         }   
         if (book.getPrice()<=0 ) { 
           errors.rejectValue("price", "price.incorrect",  "Please  
           enter a Correct correct price"); 
         } 
        if (book.getPublication().length() <=0) { 
          errors.rejectValue("publication",  
            "publication.required", 
            "Please enter publication "); 
        } 
      } 

Errors接口用于存储有关数据验证的绑定信息。errors.rejectValue()是它提供的一个非常有用的方法,它为对象及其错误消息注册错误。以下是来自Error接口的rejectValue()方法的可用签名,

      void rejectValue(String field_name, String error_code); 
      void rejectValue(String field_name, String error_code, String  
        default_message); 
      void rejectValue(String field_name, String error_code, 
        Object[] error_Args,String default_message); 

  1. AddBookController中添加一个类型为org.springframework.validation.Validator的数据成员,并用@Autowired注解进行注释,如下所示:
      @Autowired 
      Validator validator; 

  1. 更新AddControlleraddBook()方法以调用验证方法并检查是否发生了验证错误。更新后的代码如下所示:
      public ModelAndView addBook(@ModelAttribute("book") Book book,   
        BindingResult bindingResult)   throws Exception { 
        validator.validate(book, bindingResult); 
      if(bindingResult.hasErrors()) 
      { 
        return new ModelAndView("bookForm"); 
      } 
      ModelAndView modelAndView = new ModelAndView(); 
      modelAndView.setViewName("display"); 
      //later on the list will be fetched from the table 
      List<Book>books=new ArrayList(); 
      books.add(book); 
      modelAndView.addObject("book_list",books); 
      modelAndView.addObject("auth_name",book.getAuthor()); 
      return modelAndView; 
    } 

addBook()方法的签名应该有一个BindingResult作为其参数之一。BindingResult实例包含在执行验证时发生错误的消息列表。hasErrors()方法在数据成员上验证失败时返回 true。如果hasErrors()返回 true,我们将返回'bookForm'视图,使用户可以输入正确的值。在没有验证违规的情况下,将'display'视图返回给前端控制器。

  1. books-servlet.xml中如下所示注册BookValdator作为 bean:
      <bean id="validator"  
        class="com.packt.ch06.validators.BookValidator" /> 

您还可以使用@Component代替上述配置。

  1. 通过更新bookForm.jsp,如下的代码所示,显示验证违规消息给用户:
      <tr> 
        <td width="50%" align="right">Name of the Book</td> 
        <td width="50%" align="left"> 
          <form:input path="bookName" size="30" /> 
          <font color="red"> 
            <form:errors path="bookName" /> 
          </font> 
        </td> 
      </tr> 

只需在bookForm.jsp中添加下划线代码,以将消息显示为红色。

<form:errors>用于显示验证失败时的消息。它采用以下所示的语法:

      <form:errors path="name of the data_member" /> 

  1. 通过为所有输入指定数据成员的名称作为路径属性的值来更新bookForm.jsp

  2. 运行应用程序。点击添加新书籍的“显示表单”链接。

  3. 不输入任何文本字段中的数据提交表单。我们将得到显示违反哪些验证规则的消息的表单,如下所示:

上述用于验证的代码虽然可以正常工作,但我们没有充分利用 Spring 框架。调用验证方法是显式的,因为框架不知道隐式地执行验证。@Valid注解向框架提供了使用自定义验证器隐式执行验证的信息。框架支持将自定义验证器绑定到 WebDataBinder,使框架知道使用validate()方法。

使用@InitBinder 和@Valid 进行验证

让我们逐步更新AddController.java的代码,如下所示:

  1. AddBookController中添加一个方法来将验证器绑定到WebDataBinder,并用@InitBinder注解进行注释,如下所示:
      @InitBinder 
      private void initBinder(WebDataBinder webDataBinder) 
      { 
        webDataBinder.setValidator(validator); 
      } 

@InitBinder注解有助于识别执行 WebDataBinder 初始化的方法。

  1. 为了使框架考虑注解,book-servelt.xml 必须更新如下:

  2. 添加 mvc 命名空间,如下所示:

      <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:mvc="http://www.springframework.org/schema/mvc" 
        xsi:schemaLocation="http://www.springframework.org/schema/beans 
          http://www.springframework.org/schema/beans/spring-beans.xsd  
          http://www.springframework.org/schema/context  
          http://www.springframework.org/schema/context/
      spring-context.xsd 
          http://www.springframework.org/schema/mvc 
          http://www.springframework.org/schema/mvc/spring-mvc.xsd"> 

你只能复制现有代码中下划线的声明。

  1. 添加如下所示的配置:
      <mvc:annotation-driven/> 

  1. 更新addBook()方法以添加@Valid注解执行书籍验证并删除validator.validate()调用,因为它将隐式执行。更新后的代码如下所示:
      @RequestMapping("/addBook.htm") 
      public ModelAndView addBook(@Valid @ModelAttribute("book")  
      Book book,BindingResult bindingResult) 
      throws Exception { 
        //validator.validate(book, bindingResult); 
        if(bindingResult.hasErrors()) 
        { 
          return new ModelAndView("bookForm"); 
        } 
        ModelAndView modelAndView = new ModelAndView(); 
        modelAndView.setViewName("display"); 
        //later on the list will be fetched from the table 
        // rest of the code is same as the earlier implemenation 
      } 

  1. 运行应用程序,当你提交空白表单时,你会得到类似的结果。消息将在rejectValue()方法中硬编码的视图中显示。框架提供了对属性文件中外部化消息的支持。让我们更新用于外部化消息的验证器。
外部化消息

我们将使用以下步骤的外部化消息,而不改变验证逻辑:

  1. 在 com.packt.ch06.validators 包中添加一个新类 BookValidator1,实现 Validator 接口。

  2. 像早期应用程序一样覆盖 supports 方法。

  3. 覆盖我们没有提供默认错误消息的 validate 方法。我们只提供 bean 属性的名称和与之关联的错误代码,如下所示:

      public void validate(Object obj, Errors errors) { 
        Book book=(Book) obj; 
        if (book.getBookName().length() < 5) { 
          errors.rejectValue("bookName", "bookName.required"); 
        } 

        if (book.getAuthor().length() <=0) { 
          errors.rejectValue("author", "authorName.required");           
        } 

        if (book.getDescription().length() <= 0){ 
          errors.rejectValue("description","description.required");             } 

        if (book.getDescription().length() < 10 ||   
          book.getDescription().length() <  40) { 
          errors.rejectValue("description", "description.length");               } 

        if (book.getISBN()<=150l) { 
          errors.rejectValue("ISBN", "ISBN.required"); 
        } 

        if (book.getPrice()<=0 ) { 
          errors.rejectValue("price", "price.incorrect"); 
        } 

        if (book.getPublication().length() <=0) { 
          errors.rejectValue("publication", "publication.required");             } 
      } 

  1. 让我们在 WEB-INF 中添加 messages_book_validation.properties 文件,以映射错误代码到其相关的消息,如下所示:
      bookName.required=Please enter book name 
      authorName.required=Please enter name of the author 
      publication.required=Please enter publication 
      description.required=Please enter description 
      description.length=Please enter description of minimum 10 and        maximum 40 characters 
      ISBN.required=Please enter ISBN code 
      price.incorrect= Please enter correct price 

编写属性文件以映射键值对的语法如下:

      name_of_Validation_Class . name_of_model_to_validate   
        .name_of_data_memeber  = message_to_display 

  1. 更新 books-servlet.xml 如下:

  2. 注释掉为 BookValidator 编写的 bean,因为我们不再使用它

  3. 为 BookValidator1 添加一个新的 bean,如下所示:

      <bean id="validator"  
        class="com.packt.ch06.validators.BookValidator1" /> 

  1. 为 MessagSource 添加一个 bean,以从属性文件中加载消息,如下所示:
      <bean id="messageSource" 
        class="org.springframework.context.support. 
        ReloadableResourceBundleMessageSource"> 
        <property name="basename"  
          value="/WEB-INF/messages_book_validation" /> 
      </bean> 

  1. 无需更改 AddController.java。运行应用程序,提交空白表单后,将显示从属性文件中拉取的消息。

我们成功外部化了消息,恭喜 !!!

但这不是认为验证代码在这里不必要的执行基本验证吗?框架提供了 ValidationUtils 作为一个工具类,使开发人员能够执行基本验证,如空或 null 值。

使用 ValidationUtils

让我们添加 BookValidator2,它将使用 ValidationUtils 如下:

  1. 在 com.packt.ch06.validators 包中添加 BookValidator2 作为一个类,在 ReadMyBooks 应用程序中实现 Validator。

  2. 像以前一样覆盖 supports()方法。

  3. 覆盖 validate(),使用 ValidationUtils 类执行验证,如下所示:

      public void validate(Object obj, Errors errors) { 
        Book book = (Book) obj; 
        ValidationUtils.rejectIfEmptyOrWhitespace(errors,  
          "bookName", "bookName.required"); 
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "author",  
          "authorName.required"); 
        ValidationUtils.rejectIfEmptyOrWhitespace(errors,  
          "description", "description.required"); 
        if (book.getDescription().length() < 10 ||  
          book.getDescription().length() < 40) { 
          errors.rejectValue("description", "description.length", 
            "Please enter description within 40 charaters only"); 
        } 
        if (book.getISBN() <= 150l) { 
          errors.rejectValue("ISBN", "ISBN.required", "Please 
          Enter Correct ISBN number"); 
        } 
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "price",  
          "price.incorrect"); 
        ValidationUtils.rejectIfEmptyOrWhitespace(errors,  
          "publication", "publication.required"); 
      } 

  1. 由于我们重复使用相同的错误代码,因此无需在属性文件中再次添加它们。

  2. 注释掉 BookVlidator1 的 bean,并在 books-servlet.xml 中添加 BookVlidator2 的 bean,如下所示:

      <bean id="validator"  
        class="com.packt.ch06.validators.BookValidator2" /> 

  1. 执行应用程序并提交空白表单,以从属性文件中获取验证消息显示。

JSR 注解 based 验证

JSR 303 是一个 bean 规范,定义了在 J2EE 应用程序中验证 bean 的元数据和 API。市场上最新的是 JSR 349,它是 JSR 303 的扩展,提供了开放性、依赖注入和 CDI、方法验证、分组转换、与其他规范集成的特性。Hibernate Validator 是一个知名的参考实现。javax.validation.*包提供了验证目的的 API。

以下是一些在验证中常用的注解:

  • @NotNull: 检查注解的值不为空,但它不能检查空字符串。

  • @Null: 它检查注解的值是否为空。

  • @Pattern: 它检查注解的字符串是否与给定的正则表达式匹配。

  • @Past: 检查注解的值是过去的日期。

  • @Future: 检查注解的值是未来的日期。

  • @Min: 它确保注解的元素是一个数字,其值等于或大于指定的值。

  • @Max: 它确保注解的元素是一个数字,其值等于或小于指定的值。

  • @AssertFalse: 它确保注解的元素为假。

  • @AssertTrue: 它确保注解的元素为真。

  • @Size: 它确保注解的元素在最大值和最小值之间。

除了由 Bean Validation API 定义的上述注解之外,Hibernate Validator 还提供了以下附加注解:

  • @CreditCardNumber: 它检查注解的值是否遵循传递给它的字符序列。

  • @Email: 用于根据指定表达式检查特定字符是否为有效的电子邮件地址。

  • @Length: 它检查注解的元素的字符数是否受 min 和 max 属性指定的限制。

  • @NotBlank: 它检查注解的元素是否不为空且长度大于零。

  • @NotEmpty: 它确保注解的元素既不是空也不是空。

让我们通过以下步骤复制 ReadMyBooks 应用程序来实现基于 JSR 的验证:

第一部分:创建基本应用程序

  1. 创建一个名为 ReadMyBooks_JSR_Validation 的动态网页应用程序。

  2. 添加我们之前应用程序中添加的所有必需的 jar 文件。

  3. 除了这些 jar 文件外,还添加 hibernate-validator-5.0.1.final.jar、classmate-0.5.4.jar、jboss-logging-3.1.0.GA.jar 和 validation-api-1.1.0.final.jar。

  4. 复制 com.packt.ch06.beans 和 com.packt.ch06.controllers 包及其内容。

  5. 在 WebContent 目录下复制 index.jsp 和 searchByAuthor.jsp 文件。

  6. 在 web.xml 文件中添加 DispatcherServlet 映射。

  7. 在 WEB-INF 目录下复制 books-servlet.xml 文件。

  8. 复制 WEB-INF 目录下的 WebContent 和 jsps 文件夹及其内容。

第二部分:应用验证

  1. 让我们在 Book.java 上应用由 hibernate-validator API 提供的验证,如下所示:
      public class Book { 
        @NotEmpty 
        @Size(min = 2, max = 30) 
        private String bookName; 

        @Min(150) 
        private long ISBN; 

        @NotEmpty 
        @Size(min = 2, max = 30) 
        private String publication; 

        @NotNull 
        private int price; 

        @NotEmpty 
        @Size(min = 10, max = 50) 
        private String description; 

        @NotEmpty 
        private String author; 

        //default and parameterized constructor 
        //getters and setters 
      } 

  1. 让我们更新 AddBookController,如下所示:

  2. 删除 Validator 数据成员。

  3. 删除 initBinderMethod。

  4. 保持@Valid 注解应用于 addBook()方法的 Book 参数上。

  5. 从 books-servlet.xml 中删除 validator bean,因为现在它不再需要。

  6. 对 messageResource bean 进行注释,稍后我们将会使用它。

  7. 确保在 book-servlet.xml 中包含<mvc:annotation-driven />入口,以便使框架能够考虑控制器中的注解。

  8. 运行应用程序。在提交空白表单时,您将得到以下响应,显示默认的验证消息:

消息的自定义可以通过使用'message'属性来实现,或者我们可以使用属性文件外部化消息。我们逐一进行。

使用'message'属性

在 bean 类中用于验证数据的每个注解都有'message'属性。开发人员可以使用它来传递适当的消息,如下面的代码所示:

public class Book { 
  @NotEmpty(message="The book name should be entered") 
  private String bookName; 

  @Min(value=150,message="ISBN should be greater than 150") 
  private long ISBN; 

  @Size(min = 2, max = 30, message="Enter Publication between   
    limit of 2 to 30 only") 
  private String publication; 

  @NotNull(message="Enter the price") 
  private int price; 
  @Size(min = 10, max = 50,message="Enter Publication between limit of
    10 to 50 only") 
  private String description; 

  @NotEmpty(message="Enter the price") 
  private String author; 
  /default and parameterized constructor 
  //getters and setters 
} 

保持其他代码不变,按照上面所示更改 Book.java,然后运行应用程序。如果发生任何验证规则的违反,将为'message'属性配置的消息显示。

使用属性文件

开发人员可以从属性文件外部化消息,一旦验证违反,它将从中加载,就像在之前的应用程序中一样。

让我们按照以下步骤在应用程序中添加属性文件:

  1. 在 WEB-INF 中创建一个名为 messages_book_validation.properties 的文件,并添加违反规则和要显示的消息的映射,如下所示:
      NotEmpty.book.bookName=Please enter the book name F1\. 
      NotEmpty.book.author=Please enter book author F1\. 
      Min.book.ISBN= Entered ISBN must be greater than 150 F1 
      Size.book.description=Please enter book description having  
        minimum 2 and maximum 30charatcters only F1\. 
      NotNull.book.price=Please enter book price F1\. 

在每个文件的末尾故意添加了 F1,以知道消息是从 bean 类还是属性文件中拉取的。您不必在实际文件中添加它们。我们故意没有为'publication'数据成员添加任何消息,以理解消息的拉取。

编写属性文件的语法如下:

      Name_of_validator_class.name_of_model_attribute_to_validate. 
        name_of_data_member= message_to_display 

  1. 取消对 book-servlet.xml 中 bean 'messageResource'的注释,或者如果您没有,请添加一个,如下所示:
      <bean id="messageSource" 
        class="org.springframework.context.support. 
        ReloadableResourceBundleMessageSource"> 
        <property name="basename"  
          value="/WEB-INF/messages_book_validation" /> 
      </bean> 

  1. 运行应用程序,在提交空白表单时,将加载属性文件中的消息,除了'publication'之外,如下所示:

总结


我们讨论了在这款应用程序中的网络层。我们讨论了如何使用 Spring MVC 框架声明自定义控制器。我们讨论了视图如何使用从 ModelAndView 中的模型对象来显示值。我们还讨论了框架是如何发现视图,以及它们是如何通过 ViewResolvers 根据在 ModelAndView 中设置的逻辑名称进行渲染的。讨论继续深入到表单处理,我们深入讨论了如何通过使用表单支持对象和@ModelAttribute 注解来实现表单提交和预填充表单。包含错误值的表单可能会导致异常或业务逻辑失败。解决这个问题的方法是表单验证。我们通过 Spring 自定义验证器和由 Hibernate Validator 提供的基于注解的验证来讨论了表单验证。我们还发现了如何使用 messageresource 捆绑包进行外部化消息传递的方法。在下一章中,我们将继续讨论如何对应用程序进行测试,以最小化应用程序上线时失败的风险。

第七章。放心,试驾一下

应用开发是一个漫长、耗时且成本高昂的过程。开发依赖于从客户和市场收集的需求。但是,如果在工作完成后出了问题,一切都会崩溃呢?冲突并非由于解决方案错误,而是因为开发者在工作开始前基于错误的假设。这种冲突恰好在向客户交付日期前发生。现在什么都无法挽回了!我们不必深究为什么会出现问题和具体情况。但我感兴趣的是,这种情况可以避免吗?有没有什么方法可以在最后一刻避免这种冲突呢?我们总是听说“预防胜于治疗”。这个原则也适用于应用开发。通过开发人员逐步付出的少许额外努力,可以避免失败的状况。开发人员开发的代码进行交叉检查,以满足需求,这有助于确保代码的正确运行。这种交叉检查称为应用测试。在本章中,我们将通过以下几点深入讨论测试:

  • 为什么测试?

  • 测试 Spring 控制器的问题。

  • 模拟测试。

  • 春季测试上下文框架。

  • 使用 Mokitoto 测试 Spring 控制器。

  • 使用 Arquillian 介绍 Spring 控制器测试。

测试是一个重要的步骤


应用开发是一个昂贵且耗时长的过程。在最后的部署中出现的错误和失误会导致非常严重的后果。开发者根据需求编写的代码基于一些可能基于一些假设的规则。作为人类,我们可能在需求收集或假设制定上犯错误。如果这是我们完成的工作,还有谁比我们更了解它呢?单元测试测试代码,并帮助确保其正常运行。

开发者完成了开发。他们的开发基于一些假设,他们可能也会遗漏一些盲点。开发之后,他们进行测试。由同一个人进行测试是高风险的,因为他们可能会重复同样的错误。理想情况下,应该是其他人来做检查,确保他们知道他们在测试什么。

以下是一些使测试成为应用开发中难忘的一部分的主要因素:

  • 它有助于尽早发现开发过程中产生的缺陷和错误。

  • 它确保应用程序执行中的失败次数最少

  • 它有助于提高应用程序的一致性

  • 它有助于确保更好的应用程序质量

  • 它通过检查认证和授权来提高安全性

  • 帮助节省金钱,更重要的是节省时间

每个应用程序在发布之前都要经过严格的测试,以确保应用程序符合要求并确保其所有功能正确无误。单元测试、集成测试、系统测试和验收测试是每个应用程序必须通过的四个主要阶段。

单元测试

单元测试关注组件的单元,确保功能的正确性。单元可以指单个函数或过程。单元测试的主要目的是确保单元按设计工作。它允许快速解决提出的问题。由于单元是应用程序的最小部分,因此代码可以很容易地修改。通常由编写代码的开发者进行。

集成测试

一旦单元测试成功完成,测试单元时出现的大部分问题都已经修改以符合要求。集成测试提供了在程序执行内测试这些单元组的机会。它有助于确定多个单元是如何一起运行的。单元可能运行良好,但当与其他单元结合时,相同的单元可能会导致一些副作用,需要解决。集成测试有助于捕获此类错误,并有机会进行更正。

系统测试

在前两个阶段,已经对单个单元或单元之间的相互交互进行了测试。这是第一次全面测试完整应用程序的阶段。系统测试通常由独立测试员在接近生产环境中进行。系统测试确保应用程序开发的所有功能和业务要求是否已经满足。

用户验收测试

这是测试的最后阶段,它确定系统是否准备好最终发布。验收测试通常由最终用户执行,以确定应用程序符合要求并涵盖了所有必要的功能以给出最终验收。它使最终应用程序在生产环境中有了预览。

本章我们将分三个阶段进行单元测试、集成测试和系统测试。但在前进之前,让我们先了解一下市场上可用的测试工具的概况。

测试工具


以下是适用于 Java 平台的可用测试工具,

JTest

JTest 是由 Parasoft 自 1997 年以来为 Java 平台开发的自动化软件测试、编码标准合规工具。该工具利用单元测试以及集成测试。该工具便于分析类,以与 JUnit 测试用例相同的格式生成和执行测试用例。

以下是一些 JTest 功能:

  • 除了测试之外,它还涵盖了正常情况下开发者无法捕获的运行时异常。

  • 该工具还验证类是否遵循契约式设计DbC)基础。

  • 它确保代码遵循 400 个标准编码规则,并将代码与 200 个违规规则进行比对。

  • 它还可以识别功能错误、内存泄漏和安全漏洞等问题。

  • Jcontract 是 JTest 工具的一部分,它在集成测试期间验证功能需求,而不会影响应用程序的性能。

Grinder

Grinder 是一个为 Java 编程语言设计的负载测试工具,遵循 BSD 风格的开放源代码许可。它的目标是简化使用负载注入机器进行的分布式测试。它具备负载测试、能力测试、功能测试和压力测试的能力。它对系统资源的要求最低,同时在其测试上下文中管理自己的线程,如果需要可以将其分割到不同的进程。

以下是 Grinder 的特点:

  • 易于使用的基于 Java Swing 的用户界面

  • 它可以用于具有 Java API 的任何负载测试。它可以用于 Web 服务器、基于 SOAP 和 Rest API 的 Web 服务、应用服务器等。

  • Jython 和 Clojure 语言支持编写灵活、动态的测试脚本。

  • 它还管理客户端连接和 cookie。

JWalk

JWalk 是一个为 Java 平台设计的单元测试工具,支持懒惰系统性单元测试范式。它由 Anthony Simons 开发。JWalk 通过“懒惰规格”和“系统性测试”的概念来测试单个类并生成测试报告。它更适合敏捷开发,在这种开发中不需要产生正式的规格说明。通过构建和展示自动化测试用例,它能节省大量时间和精力。

以下是 JWalk 的特点:

  • 系统性地提出所有可能的测试用例。

  • 测试人员无需确认测试结果的子集。

  • 可以预测测试结果。

  • 如果类被修改,它会生成新的测试用例。

  • 适用于软件开发中的极限编程的 TDD。

PowerMock

PowerMock 是一个开源项目,作为 EasyMock 和 Mokito 框架的扩展,通过添加一些方法和注解来实现。它允许从 Java 代码中创建模拟对象的实现。有时应用程序的架构是这样设计的,它使用最终类、私有方法或静态方法来设计类。这些方法或类无法测试,因为无法创建它们的模拟对象。开发者可以选择良好的设计或可测试性。PowerMock 通过使用自定义类加载器和字节码操作,使静态方法和最终类可以被模拟。

TestNG

TestNG 是一个受 JUnit 和 NUnit 测试启发的强大测试框架,适用于单元测试、功能测试和集成测试。它支持参数化测试,这是 JUnit 不可能实现的。它配备了诸如每个测试方法(@BeforeMethod, @AfterMethod)和每个类(@BeforeClass, @AfterClass)之前和之后的数据预处理等许多有用注解。

以下 是 TestNG 的功能:

  • 易于编写测试用例

  • 它可以生成 HTML 报告

  • 它可以生成日志

  • 良好的集成测试支持

Arquillian Framework

Arquillian 是一个针对 Java 应用程序的测试框架。该框架使开发人员能够在运行时环境部署应用程序,以使用 JUnit 和 TestNG 执行测试用例。由于 Arquillian 管理以下测试生命周期管理事物,因此可以在测试内部管理运行时环境:

  • 它可以管理多个容器

  • 它使用 ShrinkWrap 捆绑类、资源和测试用例

  • 它将归档部署到容器中

  • 在容器内执行测试用例

  • 将结果返回给测试运行器

ShrinkWrap

该框架由三个主要组件组成,

测试运行器

执行测试用例时,JUnit 或 TestNG 使用 Arquillian 测试运行器。这使得在测试用例中使用组件模型成为可能。它还管理容器生命周期和依赖注入,使模型可供使用。

Java 容器

Java 容器是测试环境的主要组件。Arquillian 测试可以在任何兼容的容器中执行。Arquillian 选择容器以确定在类路径中可用的哪个容器适配器。这些容器适配器控制并帮助与容器通信。Arquillian 测试用例甚至可以在没有基于 JVM 的容器的情况下执行。我们可以使用@RunsClientto注解在 Java 容器外部执行测试用例。

将测试用例集成到 Java 容器中

该框架使用名为 ShrinkWrap 的外部依赖。它有助于定义要加载到 Java 容器中的应用程序的部署和描述符。测试用例针对这些描述符运行。Shrinkwrap 支持生成动态 Java 归档文件,类型为 JAR、WAR 和 EAR。它还可以用于添加部署描述符以及创建 DD 程序化。

Arquillian 可以在以下场景中使用,

  • 您要测试的应用程序部分需要在内嵌服务器中部署

  • 测试应在每小时、一定时间间隔后或有人提交代码时执行

  • 通过外部工具自动化应用程序的验收测试

JUnit

JUnit 是用于 Java 测试驱动开发的最受欢迎的开源框架。JUnit 有助于对组件进行单元测试。它还广泛支持诸如 ANT、Maven、Eclipse IDE 等工具。单元测试类是像其他任何类一样的普通类,主要区别在于使用@Test注解。@Test 注解让 JUnit 测试运行器知道需要执行这个注解的方法来进行测试。

org.junit.Assert 类提供了一系列静态的 assertXXX()方法,这些方法通过比较被测试方法的预期输出和实际输出来进行测试。如果测试的比较返回正常,表示测试通过了。但是,如果比较失败,执行停止,表示测试失败。

单元测试类通常被称为单元测试用例。测试用例可以有多个方法,这些方法将按照编写顺序一个接一个地执行。JUnit 为公共单元测试提供设置测试数据的功能,并针对它进行测试。数据的初始化可以在setUp()方法中完成,或者在用@Before 注解标记的方法中完成。默认情况下,它使用 JUnit 运行器来运行测试用例。但它还有 Suite、Parameterised 和 Categories 等几个内置的运行器。除了这些运行器之外,JUnit 还支持第三方运行器,如 SpringJUnit4ClassRunner、MokitoJUnitRunner、HierarchicalContextRunner。它还支持使用@RunWith 注解,以便使用自定义运行器。我们将在稍后详细讨论这个注解以及 Spring 测试框架。

以下是一些通过比较进行测试的断言方法,

  • assertEquals : 这个方法通过调用 equals()方法来测试两个对象的相等性。

  • assertTrue 和 assertFalse : 它用于将布尔值与 true 或 false 条件进行比较。

  • assertNull 和 assetNotNull : 这个方法测试值的 null 或非 null。

  • assertSame 和 assertNotSame : 它用于测试传递给它的两个引用是否指向同一个对象。

  • assertArrayEquals : 它用于测试两个数组是否包含相等的元素,并且数组中每个元素与另一个数组中相同索引的元素相等。

  • assertThat : 它用于测试对象是否与 org.hamcrest.Matcher 中的对象匹配。

第一阶段 单元测试 DAO 使用 JUnit 进行单元测试


现在,是编写实际测试用例的时候了。我们将从单元测试 DAO 层开始。以下是为编写基于注解的测试用例而遵循的一般步骤,

  1. 创建一个类,该类的名称以'Test'为前缀,紧跟被测试类的名称。

  2. 为初始化我们所需的数据和释放我们使用的资源分别编写setUp()testDown()方法。

  3. 进行测试的方法将它们的名称命名为被测试方法名称前加上'test'。

  4. 4. 测试运行器应该认识的方法的需用@Test注解标记。

  5. 使用assertXXX()方法根据测试的数据比较值。

让我们为第三章中开发的 DAO 层编写测试。我们将使用 Ch03_JdbcTemplates 作为基础项目。您可以创建一个新的项目,或者通过仅添加测试包来使用 Ch03_JdbcTemplates。让我们按照以下步骤操作:

创建基本应用程序。

  1. 创建 Ch07_JdbcTemplates_Testing 作为 Java 项目。

  2. 为 Spring 核心、Spring JDBC 和 JDBC 添加所有必需的 jar 文件,这些文件我们已经为 Ch03_JdbcTemplates 项目添加了。

  3. 从基础项目中复制 com.packt.ch03.beans 和 com.packt.ch03.dao 包。我们只对 BookDAO_JdbcTemplate 类进行测试。

  4. connection_new.xml复制到类路径中

执行测试

  1. 创建com.packt.ch07.tests

  2. 使用 Eclipse IDE 中的 JUnit 测试用例模板:

    1. 输入测试用例的名称 TestBookDAO_JdbcTemplate

    2. 为初始化和释放测试用例组件选择 setUp 和 teardown 复选框。

    3. 点击浏览按钮,选择 BookDAO_JdbcTemplate 作为测试类。

    4. 点击下一步按钮

    5. 在测试方法对话框中选择 BookDAO_JdbcTemplate 类中的所有方法。

    6. 点击完成。

    7. 将出现一个对话框,询问是否在构建路径上添加 JUnit4。点击确定按钮。

以下图表总结了这些步骤:

  1. 点击下一步按钮后,您将看到下一个对话框:

  1. 在测试用例中声明一个数据成员作为BookDAO_JdbcTemplate

  2. 更新setUp()方法,使用 ApplicationContext 容器初始化测试用例的数据成员。

  3. 更新tearDown()以释放资源。

  4. 更新testAddBook()如下:

  5. 创建一个 Book 类型的对象,并确保 ISBN 的值在 Book 表中不可用。

  6. BookDAO_JdbcTemplate类调用addBook()

  7. 使用以下代码中的assertEquals()方法测试结果:

      public classTestBookDAO_JdbcTemplate { 
        BookDAO_JdbcTemplatebookDAO_JdbcTemplate; 

        @Before 
        publicvoidsetUp() throws Exception { 
          ApplicationContextapplicationContext = new 
          ClassPathXmlApplicationContext("connection_new.xml"); 
          bookDAO_JdbcTemplate = (BookDAO_JdbcTemplate)  
          applicationContext.getBean("bookDAO_jdbcTemplate"); 
        } 
        @After 
        publicvoidtearDown() throws Exception { 
          bookDAO_JdbcTemplate = null; 
        } 

        @Test 
        publicvoidtestAddBook() { 
          Book book = newBook("Book_Test", 909090L, "Test  
          Publication", 1000, "Test Book description", "Test  
          author"); 
          introws_insert= bookDAO_JdbcTemplate.addBook(book); 
          assertEquals(1, rows_insert); 
        } 
      } 

  1. 选择testAddBook()方法并将其作为 JUnit 测试运行。

  2. 如以下图表所示,JUnit 窗口将显示一个绿色标记,表示代码已通过单元测试:

  1. 在 Book 表中,ISBN 是一个主键,如果你重新运行相同的testAddBook(),它将显示红色而不是绿色,从而失败。尽管如此,这证明了代码是根据逻辑工作的。如果测试条件之一失败,测试用例执行将停止,并显示断言错误。

注意

尝试编写一个总是通过的测试条件。

  1. 让我们添加TestAddBook_Negative ()以测试如果我们尝试添加具有相同 ISBN 的书籍会发生什么。不要忘记通过@Test注解 annotate the method。代码将如下所示:
      @Test(expected=DuplicateKeyException.class) 
      publicvoidtestAddBook_Negative() { 
        Book book = newBook("Book_Test", 909090L, "Test  
        Publication", 1000, "Test Book description", "Test  
        author"); 
        introws_insert= bookDAO_JdbcTemplate.addBook(book); 
        assertEquals(0, rows_insert); 
      } 

注意

如果添加重复键,代码将抛出 DuplicateKeyException。在@Test注解中,我们添加了DuplicateKeyException 作为期望的结果,指示 JUnit 运行器这是期望的行为。

  1. 同样,让我们将以下代码添加到其他测试方法中:
      @Test 
      publicvoidtestUpdateBook() { 
        //with ISBN which does exit in the table Book 
        longISBN = 909090L; 
        intupdated_price = 1000; 
        introws_insert = bookDAO_JdbcTemplate.updateBook(ISBN,  
          updated_price); 
        assertEquals(1, rows_insert); 
      } 
      @Test 
      publicvoidtestUpdateBook_Negative() { 
        // code for deleting the book with ISBN not in the table 
      } 
      @Test 
      publicvoidtestDeleteBook() { 
        // with ISBN which does exit in the table Book 
        longISBN = 909090L; 
        booleandeleted = bookDAO_JdbcTemplate.deleteBook(ISBN); 
        assertTrue(deleted); 
      } 
      @Test 
      publicvoidtestDeleteBook_negative() { 
        // deleting the book with no iSBN present in the table. 
      } 
      @Test 
      publicvoidtestFindAllBooks() { 
        List<Book>books =  
        bookDAO_JdbcTemplate.findAllBooks(); 
        assertTrue(books.size()>0); 
        assertEquals(4, books.size()); 
        assertEquals("Learning Modular Java  
        Programming",books.get(3).getBookName()); 
      } 
      @Test 
      publicvoidtestFindAllBooks_Author() { 
        List<Book>books =  
          bookDAO_JdbcTemplate.findAllBooks("T.M.Jog"); 
        assertEquals("Learning Modular Java  
          Programming",books.get(1).getBookName()); 
      } 

上述代码构建了几个对象,如 BookDAO_JdbcTemplate,这些对象是使用 Spring 容器构建的。在代码中,我们使用了在 setUp() 中通过 Spring 容器获得的 BookDAO_JdbcTemplate 对象。我们不能手动完成,而有更好的选择吗?是的,我们可以通过使用 Spring 提供的自定义运行器来实现。SprinJUnit4ClassRunner 是一个自定义运行器,它是 JUnit4Runner 类的扩展,提供了一个使用 Spring TestContext Framework 的设施,消除了复杂性。

Spring TestContext Framework

Spring 为开发者提供了丰富的 Spring TestContext Framework,该框架为单元测试和集成测试提供了强大的支持。它支持基于 API 的和基于注解的测试用例创建。该框架强烈支持 JUnit 和 TestNG 作为测试框架。TestContext 封装了将执行测试用例的 spring 上下文。如果需要,它还可以用于加载 ApplicationContext。TestContextManager 是管理 TestContext 的主要组件。TestContextManager 通过事件发布,而 TestExecutionListener 为发布的事件提供采取的动作。

类级注解 @RunWith 指示 JUnit 调用其引用的类来运行测试用例,而不是使用内置的运行器。Spring 提供的 SpringJUnit4ClassRunner 使 JUnit 能够使用 TestContextManager 提供的 Spring 测试框架功能。org.springframework.test.context 包提供了测试的注解驱动支持。以下注解用于初始化上下文,

@ContextConfiguration

类级注解加载了构建 Spring 容器的定义。上下文是通过引用一个类或 XML 文件来构建的。让我们逐一讨论它们:

  • 使用单个 XML 文件:
      @ContextConfiguration("classpath:connection_new.xml") 
      publicclassTestClass{ 
        //code to test 
      } 

  • 使用配置类:
      @ContextConfiguration(class=TestConfig.class) 
      publicclassTestClass{ 
        //code to test 
      } 

  • 使用配置类以及 XML 文件:
      @ContextConfiguration(locations="connection_new.xml", 
      loader=TestConfig.class) 
      publicclassTestClass{ 
        //code to test 
      } 

  • 使用上下文初始化器:
      @ContextConfiguration(initializers = 
        TestContextInitializer.class) 
      publicclassTestClass{ 
        //code to test 
      } 

@WebAppConfiguration

类级注解用于指示如何加载 ApplicationContext,并由默认位置的 WebApplicationContext(WAC)使用,文件路径为 "file:/src/main/webapp"。以下代码段显示了加载资源以初始化用于测试的 WebApplicationContext:

@WebAppConfiguration("classpath: myresource.xml") 
publicclassTestClass{ 
 //code to test 
} 

之前开发的测试用例使用显式初始化 Spring 上下文。在这个示例中,我们将讨论如何使用 SprinJUnit4ClassRunner 和 @RunWith。我们将使用 Ch07_JdbcTemplates_Testing 项目和测试 BookDAO_JdbcTemplates 的测试方法,步骤如下,

  1. 下载 spring-test-5.0.0.M1.jar 文件以使用 Spring 测试 API。

  2. 在 com.packt.ch07.tests 包中创建一个名为 SpringRunner_TestBookDAO_JdbcTemplate 的 JUnit 测试用例。选择 BookDAO_JdbcTemplate 作为测试类和其所有测试方法。

  3. 使用以下代码中的 @RunWith 和 @ContextConfiguration 注解注释类。

  4. 在代码中添加一个类型为 BookDAO 的数据成员,并应用自动装配注解,如下所示:

      @RunWith(SpringJUnit4ClassRunner.class) 
      @ContextConfiguration("classpath:connection_new.xml") 
      publicclassSpringRunner_TestBookDAO_JdbcTemplate { 
        @Autowired 
        @Qualifier("bookDAO_jdbcTemplate") 
        BookDAObookDAO_JdbcTemplate; 

        @Test 
        publicvoidtestAddBook() { 
          Book book = newBook("Book_Test", 909090L, "Test  
          Publication", 1000, "Test Book description", "Test  
          author"); 
          introws_insert = bookDAO_JdbcTemplate.addBook(book); 
          assertEquals(1, rows_insert); 
        } 
      } 

  1. @RunWith注解接受SpringJUnit4ClassRunner@ContextConfiguration接受文件以初始化容器。此外,我们使用基于注解的自动装配来测试 BookDAO 实例,而不是像早期演示中那样在setUp()方法中使用 Spring API。testAddBook()中的测试代码保持不变,因为我们没有更改逻辑。

  2. 将其作为 JUnit 测试执行,如果您的 ISBN 尚未在书籍表中可用,则测试将通过。

上述代码我们对实际数据库进行了测试,这使得它变得更慢,并且始终如此。这些测试与环境不是孤立的,并且它们总是依赖于外部依赖,在我们的案例中是数据库。单元测试案例总是根据实时值基于几个假设来编写的,以便理解处理实时值时的问题和复杂性。

我们有一个更新书籍详情的函数。要更新书籍,该函数有两个参数,第一个是接受 ISBN,第二个是使用指定的 ISBN 更新书籍的价格,如下所示:

publicintupdateBook(long ISBN, intupdated_price() 
{ 
   // code which fires the query to database and update the price     
   of the book whose ISBN has specified 
   // return 1 if book updated otherwise 0 
} 

我们编写了以下测试用例,以确定书籍是否已更新:

@Test 
public void testUpdatedBook() 
{ 
  long ISBN=2;   // isbn exists in the table 
  intupdated_price=200; 
  introws_updated=bookDAO.updateBook( ISBN, updated_price); 
  assertEquals(1, rows_updated); 
} 

我们假设 ISBN 存在于数据库中以更新书籍详情。所以,测试用例执行成功。但是,如果在其中有人更改了 ISBN,或者有人删除了具有该 ISBN 的行怎么办?我们编写的测试用例将失败。问题不在我们的测试用例中,唯一的问题是我们假设 ISBN 存在。

另外,有时实时环境可能无法访问。控制器层测试高度依赖于请求和响应对象。这些请求和响应将在应用程序部署到服务器后由容器初始化。要么服务器不适合部署,要么控制器编码所依赖的层尚未开发。所有这些问题使得测试越来越困难。这些问题使用模拟对象测试可以轻松解决。

模拟测试


模拟测试涉及使用假对象进行测试,这些对象不是真实的。这些假对象返回进行测试所需的数据。在实际对象操作中可以节省大量工作。这些假对象通常被称为“模拟对象”。模拟对象用于替换实际对象,以避免不必要的复杂性和依赖,如数据库连接。这些模拟对象与环境隔离,导致执行速度更快。通过设置数据然后指定方法的的行为来创建模拟对象。行为包括在特定场景下返回的数据。Mockito 是使用模拟对象的一个著名的测试框架。

Mockito

Mockito 是一个开源的 Java 基础应用程序测试框架,发布在 MIT 许可证下。它允许开发人员为测试驱动开发TDD)创建模拟对象,使其与框架隔离。它使用 Java 反射 API 来创建模拟对象,并具有编写测试用例的简单 API。它还允许开发人员检查方法被调用的顺序。

Mockito 有一个静态的mock()方法,可以用来创建模拟对象。它还通过使用@Mock 注解来创建模拟对象。methodMockitoAnnotations.initMocks(this)指示初始化所有由@Mock 注解的注解字段。如果我们忘记这样做,对象将是 null。@RunWith(MokitoJUnitRunner.class)也做同样的事情。MockitoJUnitRunner 是 JUnit 使用的自定义运行器。

Mockito 的工作原理是在调用函数时返回预定义的值,Mokito,when()方法提供了关于将调用哪个方法的信息,Mokito,thenXXX()用于指定函数将返回的值。以下是用以来指定要返回的值的方法,

  • thenReturn - 用于返回一个指定的值

  • thenThrow- 抛出指定的异常

  • thenthenAnswer通过用户定义的代码返回一个答案

  • thenCallRealMethod- 调用真实的方法

模拟测试是一个简单的三个步骤的过程,如下所示,

  1. 通过模拟对象初始化被测试类的依赖项

  2. 执行测试操作

  3. 编写测试条件以检查操作是否给出了预期的结果

让我们逐步使用 Mockito 创建BookDAO的模拟对象并在测试步骤中使用它,

  1. 下载 mokito-all-1.9.5.jar 并将其添加到我们用作基础项目的 Ch07_JdbeTemplate_Testing 项目中。

  2. 在 com.packt.ch07.unit_tests 包中创建Spring_Mokito_TestBookDAO_JdbcTemplate作为一个 Junit 测试用例。

  3. 添加一个类型为BookDAO的数据成员并使用@Mock 注解标注它。

  4. setup()方法中调用 Mockito 的initMocks()方法来初始化模拟对象,如下所示:

      publicclassSpring_Mokito_TestBookDAO_JdbcTemplate { 
        @Mock 
        BookDAObookDAO_JdbcTemplate; 

        @Before 
        publicvoidsetUp()throws Exception 
        { 
          MockitoAnnotations.initMocks(this); 
        } 
      } 

  1. 现在让我们添加代码来测试addBook()函数,我们首先定义期望测试函数返回的值。然后我们使用assertXXX()方法来测试以下行为:
      @Test 
      publicvoidtestAddBook() { 
        Book book = newBook("Book_Test", 909090L, "Test  
        Publication", 1000, "Test Book description",  
        "Test author"); 
        //set the behavior for values to return in our case addBook() 
        //method 
        Mockito.when(bookDAO_JdbcTemplate.addBook(book)).thenReturn(1); 

        // invoke the function under test 
        introws_insert = bookDAO_JdbcTemplate.addBook(book); 

        // assert the actual value return by the method under test to        
        //the expected behaiour by mock object 
        assertEquals(1, rows_insert); 
      } 

  1. 执行测试用例并测试行为。我们将得到所有测试用例成功执行。

  2. 接下来让我们也添加findAllBooks(String)deleteBook()方法的其他代码:

      @Test 
      publicvoidtestDeleteBook() { 

        //with ISBN which does exit in the table Book 
        longISBN = 909090L; 
        Mockito.when(bookDAO_JdbcTemplate.deleteBook(ISBN)). 
          thenReturn(true); 
        booleandeleted = bookDAO_JdbcTemplate.deleteBook(ISBN); 
        assertTrue(deleted); 
      } 

      @Test 
      publicvoidtestFindAllBooks_Author() { 
        List<Book>books=newArrayList(); 
        books.add(new Book("Book_Test", 909090L, "Test  
          Publication", 1000, "Test Book description", "Test  
          author") ); 

        Mockito.when(bookDAO_JdbcTemplate.findAllBooks("Test  
          author")).thenReturn(books); 
        assertTrue(books.size()>0); 
        assertEquals(1, books.size()); 
        assertEquals("Book_Test",books.get(0).getBookName()); 
      } 

在之前的示例中,我们讨论了在实时环境以及在使用模拟对象时 DAO 层的单元测试。现在让我们在接下来的部分使用 Spring MVC 测试框架来测试控制器。

使用 Spring TestContext 框架进行 Spring MVC 控制器测试

Mockito 为开发人员提供了创建 DAO 层模拟对象的功能。在前面的讨论中,我们没有 DAO 对象,但即使没有它,测试也是可能的。没有模拟对象,Spring MVC 层测试是不可能的,因为它们高度依赖于初始化由容器完成的请求和响应对象。spring-test 模块支持创建 Servlet API 的模拟对象,使在不实际部署容器的情况下测试 Web 组件成为可能。以下表格显示了由 Spring TestContext 框架提供的用于创建模拟对象包列表:

包名 提供模拟实现
org.springframework.mock.env 环境和属性源
org.springframework.mock.jndi JNDI SPI
org.springframework.mock.web Servlet API
org.springframework.mock.portlet Portlet API

org.springframework.mock.web 提供了 MockHttpServletRequest,MockHttpServletResponse,MockHttpSession 作为 HttpServletRequest,HttpServletResponse 和 HttpSession 的模拟对象,供使用。它还提供了 ModelAndViewAssert 类,以测试 Spring MVC 框架中的 ModelAndView 对象。让我们逐步测试我们的 SearchBookController 如下:

  1. 将 spring-test.jar 添加到ReadMyBooks应用程序中,我们将在测试中使用它。

  2. 创建com.packt.ch06.controllers.test_controllers包,以添加控制器的测试用例。

  3. 在先前步骤创建的包中创建TestSearchBookController作为 JUnit 测试用例。

  4. 使用@WebAppConfiguration进行注解。

  5. 声明类型为 SearchBookController 的数据成员并如代码所示自动注入:

      @WebAppConfiguration 
      @ContextConfiguration({ "file:WebContent/WEB-INF/book- 
        servlet.xml" }) 
      @RunWith(value = SpringJUnit4ClassRunner.class) 
      publicclassTestSearchBookController { 
         @Autowired 
        SearchBookControllersearchBookController; 
      } 

  1. 让我们测试 add testSearchBookByAuthor()以测试 searchBookByAuthor()方法。该方法接受用户在 Web 表单中输入的作者名称,并返回该作者所写的书籍列表。代码将如下所示:

    1. 初始化测试方法所需的数据

    2. 调用测试方法

    3. 断言值。

  2. 最终代码将如下所示:

      @Test 
      publicvoidtestSearchBookByAuthor() { 

        String author_name="T.M.Jog"; 
        ModelAndViewmodelAndView =   
          searchBookController.searchBookByAuthor(author_name); 
        assertEquals("display",modelAndView.getViewName()); 
      } 

  1. 我们正在测试名为'display'的视图名称,该视图是从控制器方法中编写出来的。

  2. Spring 提供了 ModelAndViewAssert,提供了一个测试控制器方法返回的 ModelAndView 的方法,如下面的代码所示:

      @Test 
      publicvoidtestSerachBookByAuthor_New() 
      { 
        String author_name="T.M.Jog"; 
        List<Book>books = newArrayList<Book>(); 
        books.add(new Book("Learning Modular Java Programming",  
          9781235, "packt pub publication", 800, 
          "explore the power of modular Programming ", author_name)); 
        books.add(new Book("Learning Modular Java Programming",  
          9781235, "packt pub publication", 800, 
          "explore the power of modular Programming ", author_name)); 
        ModelAndViewmodelAndView = 
          searchBookController.searchBookByAuthor(author_name); 
        ModelAndViewAssert.assertModelAttributeAvailable( 
          modelAndView, "book_list"); 
      } 

  1. 执行测试用例,绿色表示测试用例已通过。

  2. 我们成功测试了 SearchBookController,其具有无需任何表单提交、表单模型属性绑定、表单验证等简单编码。我们刚刚处理的这些复杂的代码测试变得更加复杂。

Spring MockMvc

Spring 提供了 MockMVC,作为主要的入口点,并配备了启动服务器端测试的方法。将使用 MockMVCBuilder 接口的实现来创建一个 MockMVC 对象。MockMVCBuilders 提供了以下静态方法,可以获取 MockMVCBuilder 的实现:

  • xmlConfigSetUp(String ...configLocation) - 当使用 XML 配置文件来配置应用程序上下文时使用,如下所示:
      MockMvcmockMVC=   
      MockMvcBuilders.xmlConfigSetUp("classpath:myConfig.xml").build(); 

  • annotationConfigSetUp(Class ... configClasses) - 当使用 Java 类来配置应用程序上下文时使用。以下代码显示了如何使用 MyConfig.java 作为一个配置类:
      MockMvcmockMVC=  
         MockMvcBuilders.annotationConfigSetUp(MyConfiog.class). 
                                                         build(); 

  • standaloneSetUp(Object ... controllers) - 当开发者配置了测试控制器及其所需的 MVC 组件时使用。以下代码显示了使用 MyController 进行配置:
      MockMvcmockMVC= MockMvcBuilders.standaloneSetUp( 
        newMyController()).build(); 

  • webApplicationContextSetUp(WebApplicationContext context) - 当开发者已经完全初始化 WebApplicationContext 实例时使用。以下代码显示了如何使用该方法:
      @Autowired 
      WebApplicationContextwebAppContext; 
      MockMvcmockMVC= MockMVCBuilders.webApplicationContextSetup( 
        webAppContext).build(); 

MockMvc has perform() method which accepts the instance of RequestBuilder and returns the ResultActions. The MockHttpServletRequestBuilder is an implementation of RequestBuilder who has methods to build the request by setting request parameters, session. The following table shows the methods which facilitate building the request,

Method name The data method description
accept 用于将“Accept”头设置为给定的媒体类型
buildRequest 用于构建 MockHttpServletRequest
createServletRequest 根据 ServletContext,该方法创建一个新的 MockHttpServletRequest
Param 用于将请求参数设置到 MockHttpServletRequest。
principal 用于设置请求的主体。
locale . 用于设置请求的区域设置。
requestAttr 用于设置请求属性。
Session, sessionAttr, sessionAttrs 用于设置会话或会话属性到请求
characterEncoding 用于将字符编码设置为请求
content and contentType 用于设置请求的正文和内容类型头。
header and headers 用于向请求添加一个或所有头信息。
contextPath 用于指定表示请求 URI 的上下文路径部分
Cookie 用于向请求添加 Cookie。
flashAttr 用于设置输入的闪存属性。
pathInfo 用于指定表示请求 URI 的 pathInfo 部分。
Secure 用于设置 ServletRequest 的安全属性,如 HTTPS。
servletPath 用于指定表示 Servlet 映射路径的请求 URI 部分。

The perfom() method of MockMvc returns the ResultActions, which facilitates the assertions of the expected result by following methods:

Method name Description
andDo 它接受一个通用操作。
andExpect 它接受预期的操作
annReturn 它返回预期请求的结果,可以直接访问。

Let's use MockMvc to test AddBookController step by step:

  1. Add TestAddBookController as JUnit test case in com.packt.ch06.controllers.test_controllers package.

  2. 像早期代码中一样,用@WebAppConfiguration@ContextConfiguration@RunWith注解类。

  3. 添加类型为 WebApplicationContext 和AddBookController的数据成员,并用@Autowired注解两者。

  4. 添加类型为 MockMvc 的数据成员,并在 setup()方法中初始化它,如以下所示释放内存:

      @WebAppConfiguration 
      @ContextConfiguration( 
        { "file:WebContent/WEB-INF/books-servlet.xml"}) 
      @RunWith(value = SpringJUnit4ClassRunner.class) 
      publicclassTestAddBookController { 
        @Autowired 
        WebApplicationContextwac; 

        MockMvcmockMVC; 

        @Autowired 
        AddBookControlleraddBookController; 

        @Before 
        publicvoidsetUp() throws Exception { 
          mockMVC= 
            MockMvcBuilders.standaloneSetup(addBookController).build(); 
        } 
      } 

  • 让我们在 testAddBook()中添加测试 addBook()方法的代码:

    1. 通过设置以下值初始化请求:
  • 模型属性'book'使用默认值

  • 将表单提交结果设置为内容类型

  • 方法将被调用的 URI

  • 表单的请求参数:

    1. 通过检查测试结果:
  • 视图名称

  • 模型属性名称

    1. 使用 andDo()在控制台上打印测试动作的结果

测试 AddBook()方法的代码如下:

      @Test 
      publicvoidtestAddBook() { 
        try { 
          mockMVC.perform(MockMvcRequestBuilders.post("/addBook.htm") 
          .contentType(MediaType.APPLICATION_FORM_URLENCODED) 
          .param("bookName", "Book_test") 
          .param("author", "author_test") 
          .param("description", "adding book for test") 
          .param("ISBN", "1234") 
          .param("price", "9191") 
          .param("publication", "This is the test publication") 
          .requestAttr("book", new Book())) 
          .andExpect(MockMvcResultMatchers.view().name("display")) 
          .andExpect(MockMvcResultMatchers.model(). 
            attribute("auth_name","author_test")) 
          .andDo(MockMvcResultHandlers.print()); 
        } catch (Exception e) { 
          // TODO: handle exception 
          fail(e.getMessage()); 
        } 
      } 

在 andExpect( )中的预期行为匹配由 ResultMatcher 提供。MockMvcResultMatcher 是 ResultMatcher 的一个实现,提供了匹配视图、cookie、header、模型、请求和其他许多参数的方法。andDo()方法将 MvcResult 打印到 OutputStream。

  1. 运行测试用例,令人惊讶的是它会失败。输出的一部分如下所示:

  1. 它显示了验证错误,但我们已经根据验证规则给出了所有输入。哪个验证失败了从输出中看不清楚。不,没必要惊慌,也不需要逐个检查验证。

  2. 与其制造更多混乱,不如添加使用 attributeHasErrors()的验证测试代码,如下划线语句所示:

      @Test 
publicvoidtestAddBook_Form_validation() { 
        try { 
          mockMVC.perform(MockMvcRequestBuilders.post("/addBook.htm")                        .contentType(MediaType.APPLICATION_FORM_URLENCODED) 
          .param("bookName", "Book_test") 
          .param("author", "author_test") 
          .param("description", "adding book for test") 
          .param("ISBN", "12345") 
          .param("price", "9191") 
          .param("publication", "This is the test publication") 
          .requestAttr("book", new Book())) 
          .andExpect(MockMvcResultMatchers.view().name("bookForm")) 
          .andExpect(MockMvcResultMatchers .model(). 
            attributeHasErrors("book")) 
          .andDo(MockMvcResultHandlers.print()); 
        }  
        catch (Exception e) { 
          fail(e.getMessage()); 
          e.printStackTrace(); 
        } 
      }  

  1. 测试运行成功,证明输入存在验证错误。我们可以在控制台输出的'errors'中获取到验证失败的字段:
      MockHttpServletRequest: 
        HTTP Method = POST 
        Request URI = /addBook.htm 
        Parameters = {bookName=[Book_test],  
      author=[author_test], 
      description=[adding book for test],  
      ISBN=[1234],  
      price=[9191], 
      publication=[This is the test publication]} 
      Headers = { 
        Content-Type=[application/x-www-form-urlencoded]} 
      Handler: 
        Type = com.packt.ch06.controllers.AddBookController 
        Method = public  
      org.springframework.web.servlet.ModelAndView 
      com.packt.ch06.controllers.AddBookController. 
      addBook(com.packt.ch06.beans.Book,org. 
      springframework.validation.BindingResult)       
      throwsjava.lang.Exception 
      Async: 
      Async started = false 
      Async result = null 

      Resolved Exception: 
        Type = null 
      ModelAndView: 
        View name = bookForm 
        View = null 
        Attribute = priceList 
        value = [300, 350, 400, 500, 550, 600] 
        Attribute = book 
        value = Book_test  adding book for test  9191 
        errors = [Field error in object 'book' on field  
          'description':  
          rejected value [adding book for test];  
          codes 
          [description.length.book.description, 
          description.length.description,description. 
          length.java.lang.String,description.length]; 
          arguments []; 
          default message [Please enter description  
          within 40 charaters only]] 
      FlashMap: 
        Attributes = null 
      MockHttpServletResponse: 
        Status = 200 
      Error message = null 
      Headers = {} 
      Content type = null 
      Body =  
      Forwarded URL = bookForm 
      Redirected URL = null 
      Cookies = [] 

  1. 尽管描述符中的字符在 10 到 40 个指定字符的限制内。让我们找出在 Validator2 中犯错的规则。

  2. 设置发布验证规则的 validate 方法中的代码是:

      if (book.getDescription().length() < 10 ||   
        book.getDescription().length() < 40)  
      { 
        errors.rejectValue("description", "description.length", 
          "Please enter description within 40 charaters only"); 
      } 

  1. 是的,我们将发布长度设置为小于 40 的验证,导致失败。我们犯了一个错误。让我们更改代码,以设置规则,长度大于 40 将不允许。以下是更新的代码:
      if (book.getDescription().length() < 10 ||
        book.getDescription().length() > 40)  
      { 
        errors.rejectValue("description", "description.length", 
        "Please enter description within 40 charaters only"); 
      } 

  1. 现在重新运行 testAddController 以查看发生了什么。

  2. 测试用例成功通过。这就是我们进行测试用例的原因。

  3. 现在让我们在 testAddBook_Form_validation()中添加测试字段验证的代码:

      @Test 
      publicvoidtestAddBook_Form_Field_Validation() 
      { 
        try { 
          mockMVC.perform(MockMvcRequestBuilders.post("/addBook.htm") 
          .param("bookName", "") 
          .param("author", "author_test") 
          .param("description"," no desc") 
          .param("ISBN", "123") 
          .param("price", "9191") 
          .param("publication", " ") 
          .requestAttr("book", new Book())) 
          .andExpect(MockMvcResultMatchers.view().name("bookForm"))  
          .andExpect(MockMvcResultMatchers.model() 
          .attributeHasFieldErrors("book", "description")).andExpect(
            MockMvcResultMatchers.model() 
          .attributeHasFieldErrors("book", "ISBN")).andExpect( 
            MockMvcResultMatchers.model() 
          .attributeHasFieldErrors("book", "bookName")). 
            andDo(MockMvcResultHandlers.print()); 
        }catch(Exception ex) 
        { 
          fail(ex.getMessage()); 
        } 
      } 

  1. 运行测试用例,其中验证错误失败。

控制器和 DAO 正常工作。服务层使用 DAO,所以让我们对服务层进行集成测试。您可以按照我们讨论的和对 DAO 层测试进行模拟对象测试。我们将进入服务层集成测试的下一阶段。

第二阶段 集成测试


服务和 DAO 层的集成测试

让我们逐步进行应用程序的集成测试,Ch05_Declarative_Transaction_Management 如下:

  1. 创建 com.packt.ch05.service.integration_tests 包。

  2. 创建 JUnit 测试用例 TestBookService_Integration,将 BookServiceImpl 作为测试类。选择其所有方法进行测试。

  3. 声明类型为 BookService 的数据成员,并用@Autowired 注解注释它,如下所示:

      @RunWith(SpringJUnit4ClassRunner.class) 
      @ContextConfiguration("classpath:connection_new.xml") 
      publicclassTestBookService_Integration 
      { 
        @Autowired 
        BookServicebookService; 
      }   

  1. 让我们测试 addBook()方法,就像我们之前在 JUnit 测试中做的那样。你可以参考下面的代码:
      @Test 
      publicvoidtestAddBook() { 
        // Choose ISBN which is not there in book table 
        Book book = newBook("Book_Test", 909098L, "Test  
        Publication", 1000, "Test Book description", "Test  
        author"); 
        booleanflag=bookService.addBook(book); 
        assertEquals(true, flag); 
      } 

  1. 你可以运行测试用例,它将成功运行。

注意

BookService 中的所有其他测试方法可以从源代码中参考。

我们开发的两个层次都在按我们的预期工作。我们分别开发了控制器、服务和 DAO,并进行了测试。现在,我们将它们组合到单个应用程序中,这样我们就会有一个完整的应用程序,然后通过集成测试,我们将检查它是否如预期般工作。

控制器和 Service 层的集成测试

让我们将以下三个层次从 Ch05_Declarative_Transaction_Management 中组合到 ReadMyBooks 中:

  1. 在 ReadMyBooks 的 lib 文件夹中添加 jdbc 和 spring-jdbc 以及其他所需的 jar 文件。

  2. 从 Ch05_Declarative_Transaction_Management 中将 com.packt.ch03.dao 和 com.packt.ch05.service 包复制到 ReadMyBooks 应用程序。

  3. 在 ReadMyBooks 应用程序的类路径中复制 connection_new.xml。

  4. 在 Book 类的表单提交中,我们注释了默认构造函数,服务中的 addBook 逻辑是检查 98564567las 的默认值。

  5. 如下所示,通过下划线修改 BookService,其余代码保持不变:

      @Override 
      @Transactional(readOnly=false) 
      publicbooleanaddBook(Book book) { 
        // TODO Auto-generated method stub 

        if (searchBook(book.getISBN()).getISBN() == 0) { 
          // 
        } 
      } 

  1. 控制器需要更新以与底层层进行通信,如下所示:

    • 在控制器中添加类型为 BookService 的自动装配数据成员。

    • 根据业务逻辑要求,在控制器的 method 中调用服务层的 method。

  2. 下面将更新 addBook()方法:

      @RequestMapping("/addBook.htm") 
      publicModelAndViewaddBook(@Valid@ModelAttribute("book") 
      Book book, BindingResultbindingResult) 
      throws Exception { 
        // same code as developed in the controller 
        // later on the list will be fetched from the table 
        List<Book>books = newArrayList(); 

        if (bookService.addBook(book)) { 
          books.add(book); 
        } 
        modelAndView.addObject("book_list", books);   
        modelAndView.addObject("auth_name", book.getAuthor());  
        returnmodelAndView; 
      } 

注意

同样,我们可以更新所有控制器中的方法。你可以参考完整的源代码。

让我们执行测试用例 TestAddBookController.java 以获取结果。

代码将执行并给出成功消息。同时在表中添加了一行,包含了我们指定的 ISBN 和其他值。

我们已经成功测试了所有组件。现在我们可以直接开始系统测试。

但是要有耐心,因为我们将在测试框架“Arquillian”中讨论新的条目。

第三阶段系统测试


现在所有层次都按照预期工作,是时候通过网络测试应用程序了,即逐一检查功能,同时非常注意逐步进行,不仅要关注结果,还要观察演示,这将接近实际的部署环境。让我们部署应用程序,以检查所有功能是否正常工作,并在数据库和演示方面给出正确的结果,通过以下任一方式进行:

使用 Eclipse IDE 进行部署

在 Eclipse 中,一旦开发完成,配置服务器并从项目浏览器中选择项目以选择Run on server选项,如下面的箭头所示:

IDE 会将应用程序包装在战争文件中,并将其部署到容器中。现在,你可以逐一检查功能,以确保一切按预期进行。我们还将关注演示文稿、外观和准确性的数据,这些数据由演示文稿显示。

手动部署应用程序

手动部署应用程序可以通过以下步骤进行:

  1. 首先,我们需要获取它的 jar 文件。我们可以使用 Eclipse IDE 通过右键点击应用程序并选择Export来轻松获取战争文件,如下面的箭头所示:

  1. 选择你想要创建战争文件的目标位置。如果你想的话,可以更改战争文件的名字。我将保持 ReadMyBooks 不变。

  2. 点击finish完成过程。你将在选定的目标位置得到一个战争文件。

  3. 复制我们在上一步创建的 WAR 文件,并将其粘贴到 Tomcat 目录下的'webapps'文件夹中。

  4. 通过点击bin文件夹中的startup.bat文件来启动 tomcat。

  5. 一旦 tomcat 启动,打开浏览器并输入主页 URL,格式为host_name:port_number_of_tomcat/war_file_name。在我们的案例中,它是locathost:8080/ReadMyBooks

  6. 在继续之前,请确保数据库参数已正确设置,否则应用程序将失败。

  7. 主页将打开,我们可以在这里测试应用程序的功能和外观。

摘要


在本章中,我们讨论了什么是测试以及为什么它如此重要。我们还讨论了单元测试、集成测试和用户接受测试作为测试的阶段。市场上有很多测试工具,我们对此进行了概述,以便您明智地选择工具。测试中一个非常重要的工具是'Junit 测试',我们使用它来执行 DAO 层的单元测试,这是测试阶段 1 的开始。但是 JUnit 使用实时数据库,我们讨论了在外部参数上测试的困难。我们通过使用模拟对象解决了这个问题。Mokito 是创建模拟对象的工具之一,我们探索它来测试 DAO 层。在 DAO 层之后,我们测试了 Web 层,这也依赖于 Web 容器来初始化请求和响应对象。我们深入讨论了 Spring TestContext 框架,其 MockMVC 模块便于创建 Web 相关组件(如请求和响应)的模拟对象。我们还使用该框架进行表单验证测试。在单元测试之后,我们执行了 DAO 和 Service 层的集成测试,然后是 Web 和 Service 层的集成测试。故事不会在这里结束,我们通过进行系统测试来成功部署并最终检查产品。我们所开发的的所有组件都在正常工作,我们通过成功执行系统测试证明了这一点!!

在下一章中,我们将进一步讨论安全性在应用程序中的角色以及 Spring 框架提供的实现安全性的方法。请继续阅读!!!

第八章.探索 Restful 网络服务的强大功能

在之前的章节中,我们讨论了关于构建 Spring MVC 应用程序。这些应用程序通过网络只为 Java 平台提供服务。如果其他平台想要使用我们开发的功能会怎样?是的,我们需要平台无关的功能。在本章中,我们将讨论如何使用 Restful 网络服务开发此类平台无关的服务,以解决以下主题:

  • 网络服务是什么?

  • 网络服务的重要性。

  • 网络服务类型

  • Restful 网络服务

  • 开发 Spring restful 网络服务。

  • 如何使用 RestTemplate 和 POSTMAN 测试网络服务?

  • 使用消息转换器和内容协商来展示数据。

网络服务


网络服务是两个或更多为不同平台开发的应用程序之间的通信方式。这些服务不受浏览器和操作系统的限制,使得通信更加容易,性能得到增强,能够吸引更多用户。这种服务可以是一个函数,一系列标准或协议,部署在服务器上。它是客户端和服务器之间或通过网络两个设备之间的通信。比如说我们用 Java 开发了一个服务并将其发布到互联网上。现在这个服务可以被任何基于 Java 的应用程序消费,但更重要的是,任何基于.NET 或 Linux 的应用程序也可以同样轻松地消费它。这种通信是通过基于 XML 的消息和 HTTP 协议进行的。

为什么我们需要网络服务?

互操作性是网络服务可以实现的最佳功能之一,除此之外,它们还提供以下功能

可用性

许多应用程序在开发已经存在于其他应用程序中的复杂功能时投入了宝贵的时间。 Instead of redeveloping it, 网络服务允许开发人员探索通过网络暴露的此类服务。它还允许开发人员复用 Web 服务,节省宝贵的时间,并开发定制的客户端逻辑。

复用已开发的应用程序

技术市场变化如此之快,开发者必须不断跟上客户需求。在开发中,重新开发一个应用以支持新特性是非常常见的,只需20 min就能深入理解知识点,而且记忆深刻,难以遗忘。 Instead of developing the complete application from scratch, 开发者现在可以添加他们想要的任何平台上的增强功能,并使用 web 服务来使用旧模块。

松耦合模块

每个作为网络服务开发的服务的完全独立性,支持轻松修改它们,而不会影响应用程序的其他部分。

部署的便捷性

网络服务部署在服务器上,通过互联网使用。网络服务可以通过互联网部署在防火墙后面,与在本地服务器上部署一样方便。

网络服务类型

SOAP 网络服务

RESTful 网络服务

面向对象状态转换(RESTful)网络服务是一种架构风格。RESTful 资源是围绕数据的某种表示形式进行转换。REST 资源将采用适合消费者的形式。它可以是 XML、JSON 或 HTML 等表示形式。在 RESTful 网络服务中,资源的状态比针对资源采取的动作更重要。

RESTful 网络服务的优点:

  • RESTful 网络服务因其消耗较少资源而快速。

  • 它可以编写并在任何平台上执行。

  • 最重要的是,它允许不同的平台,如 HTML、XML、纯文本和 JSON。

在 Spring 中使用 RESTful 网络服务

Spring 支持编写 RestController,该控制器可以使用@RestController 注解处理 HTTP 请求。它还提供了@GetMapping、@PostMapping、@DeleteMapping、@PutMapping 注解来处理 HTTP get、post、delete 和 put 方法。@PathVariable 注解有助于从 URI 模板访问值。目前,大多数浏览器支持使用 GET 和 POST 作为 HTTP 方法和 html 动作方法。HiddenHttpMethodFilter 现在允许使用form:form标签提交 PUT 和 DELETE 方法的表单。Spring 使用 ContentNegotiatingViewResolver 根据请求的媒体类型选择合适的视图。它实现了已经用于 Spring MVC 的 ViewResolver。它自动将请求委托给适当的视图解析器。Spring 框架引入了@ResponseBody 和@RequestBody,以将方法参数绑定到请求或响应。客户端与服务器之间的请求和响应通信读写多种格式的数据,可能需要消息转换器。Spring 提供了许多消息转换器,如 StringHttpMessageConverter、FormHttpMessageConverter、MarshallingHttpMessageConverter,以执行读写操作。RestTemplate 提供了易于消费 RESTful 网络服务的客户端端。

在继续前进之前,让我们通过以下步骤开发一个 RESTController,以理解流程和 URI 消耗:

  1. 创建 Ch09_Spring_Restful 动态网络应用程序,并添加为 Spring web MVC 应用程序添加的 jar 文件。

  2. 在 web.xml 文件中将 DispatcherServlet 作为前端控制器映射,如下所示,以映射所有 URL:

        <servlet> 
          <servlet-name>books</servlet-name> 
            <servlet-class>     
              org.springframework.web.servlet.DispatcherServlet 
            </servlet-class> 
          </servlet> 
        <servlet-mapping> 
          <servlet-name>books</servlet-name> 
          <url-pattern>/*</url-pattern> 
        </servlet-mapping> 

  1. 在每一个 Spring web MVC 应用程序中添加 books-servlet.xml,以配置基本包名,以便扫描控制器和服务器视图解析器,这是我们添加的。

  2. 在 com.packt.ch09.controllers 包中创建MyRestController类。

  3. 使用@RestController 注解标注类。

  4. 为消耗'/welcome' URI 添加getData()方法,如下面的代码所示:

@RestController 
public class MyRestController { 

  @RequestMapping(value="/welcome",method=RequestMethod.GET) 
  public String getData() 
  { 
    return("welcome to web services"); 
  } 
} 

getData()方法将为'/welcome' URL 的 GET HTTP 方法提供服务,并返回一个字符串消息作为响应。

  1. 将应用程序部署到容器中,一旦服务成功部署,是时候通过创建客户端来测试应用程序了。

  2. 让我们使用 Spring 提供的RestTemplate编写客户端,如下所示:

        public class Main { 
          public static void main(String[] args) { 
           // TODO Auto-generated method stub 
           String URI=    
             "http://localhost:8080/Ch09_Spring_Restful/welcome" 
           RestTemplate template=new RestTemplate(); 
           System.out.println(template.getForObject(URI,String.class)); 
          } 
        } 

执行主函数将在您的控制台显示“欢迎来到 Web 服务”。

RestTemplate

与许多其他模板类(如 JdbcTemplate 和 HibernateTemplate)类似,RestTemplate 类也设计用于执行复杂功能,以调用 REST 服务。以下表格总结了 RestTemplate 提供的映射 HTTP 方法的方法:

RestTemplate 方法 HTTP 方法 描述
getForEntity 和 getForObject GET 它检索指定 URI 上的表示
postForLocation 和 postForObject POST 它通过在指定的 URI 位置发布新对象来创建新资源,并返回值为 Location 的头部
put PUT 它在指定的 URI 上创建或更新资源
delete DELETE 它删除由 URI 指定的资源
optionsForAllow OPTIONS 该方法返回指定 URL 的允许头部的值。
execute 和 exchange 任何 执行 HTTP 方法并返回作为 ResponseEntity 的响应

我们将在接下来的演示中覆盖大部分内容。但在深入 RESTful Web 服务之前,让我们讨论 RESTful Web 服务的最重要部分——URL。RestContollers 仅处理通过正确 URL 请求的请求。Spring MVC 控制器也处理参数化和查询参数的 Web 请求,而由 RESTful Web 服务处理的 URL 是面向资源的。通过没有查询参数的整个基本 URL 来完成对要映射的资源的识别。

所写的 URL 基于其中的复数名词,并尝试避免使用动词或查询参数,就像我们在 Spring MVC 早期演示中一样。让我们讨论 URL 是如何形成的。以下是一个资源的 RESTful URL,它是 Servlet 上下文、要获取的资源名词和路径变量的组合:

观察以下表格以了解更多关于 RESTful URL 的信息:

支持的 HTTP 方法 要获取的资源 GET 方法 POST 方法 PUT 方法 DELETE 方法
/books 返回书籍列表 添加新书 更新书籍或书籍 删除书籍
/books/100 返回书籍 405 更新书籍 删除书籍

让我们通过以下步骤开发一个应用程序,使用不同的 HTTP 方法和 URL 以更好地理解。在这个应用程序中,我们将使用 Ch03_JdbcTemplate 作为我们的数据访问层,从这里你可以直接复制所需的代码。

  1. 创建 Ch09_Restful_JDBC 文件夹,并添加所有必需的 jar 包,如 WebContent 文件夹大纲所示:

  1. 像在早期应用程序中一样,在 web.xml 和 books-servlet.xml 中添加前端控制器和 web 组件映射文件。您可以从早期应用程序中复制相同的文件。不要忘记添加 'contextConfigLocation',因为我们正在编写多个 bean 配置文件。

  2. 在 com.ch03.beans 中添加 Book.java 作为 POJO,这是我们所有 JDBC 应用程序中使用过的。

  3. 添加包含 BookDAO 和 BookDAO_JdbcTemplate 类的 com.packt.cho3.dao 包。

  4. 在类路径中添加 connection_new.xml。

  5. 在 com.packt.ch09.controllers 包中创建 MyBookController 类,并用 @RestController 注解标记它。

  6. 将 BookDAO 作为数据成员添加,并用 @Autowired 注解标记它。

  7. 现在,我们将添加 getBook() 方法来处理搜索书籍的网络服务请求。用 @GetMapping 注解 URL '/books/{ISBN}' 的方法,如下代码所示:

        @RestController 
        @EnableWebMvc 
        public class MyBookController { 

          @Autowired 
          BookDAO bookDAO; 
          @GetMapping("/books/{ISBN}") 
          public ResponseEntity getBook(@PathVariable long ISBN) { 

            Book book = bookDAO.getBook(ISBN); 
            if (null == book) { 
              return new ResponseEntity<Book>(HttpStatus.NOT_FOUND); 
            } 

            return new ResponseEntity(book, HttpStatus.OK); 
          } 
        } 

@GetMapping 设置方法来处理以 'books/{ISBN}' 形式的 URL 的 GET 请求。{name_of_variable} 作为占位符,以便将数据传递给方法以供使用。我们还使用了应用于方法签名中的第一个参数的 @PathVariable 注解。它有助于将 URL 变量的值绑定到参数。在我们的案例中,ISBN 有通过 URL 的 ISBN 传递的值。

HttpStatus.NO_CONTENT 状态表示要设置响应的状态,指示资源已被处理,但数据不可用。

ResponseEntity 是 HttpEntity 的扩展,其中包含了关于 HttpStatus 的响应的附加信息。

让我们添加使用 RestTemplate 访问映射资源的客户端代码,如下所示:

        public class Main_Get_Book { 
          public static void main(String[] args) { 
            // TODO Auto-generated method stub 

            RestTemplate template=new RestTemplate(); 
            Book book=   
             template.getForObject( 
               "http://localhost:8081/Ch09_Spring_Rest_JDBC/books/14", 
               Book.class); 
            System.out.println(book.getAuthor()+"\t"+book.getISBN()); 
          } 
        } 

在这里,我们获取 ISBN=14 的书籍。确保表中存在此 ISBN,如果没有,您可以添加自己的值。

执行 Main_Get_Book 以在控制台获取书籍详细信息。

我们可以使用以下步骤使用 POSTMAN 工具测试 Google Chrome 中的 RESTful web 服务:

  1. 您可以在 Google Chrome 中安装 Postman REST 客户端,网址为 chrome.google.com/webstore/detail/postman-rest-client/fdmmgilgnpjigdojojpjoooidkmcomcm

  2. 一旦安装,通过点击 Postman 图标来启动它。

  3. 现在,从下拉菜单中选择 GET 方法,并在文本字段中输入 URL http://localhost:8081/Ch09_Spring_Rest_JDBC/books/13。

  4. 点击“发送”按钮。

  5. 通过下面的图片显示的身体中的列表,我们将获得如下所示的数据:

URL 只指定了处理请求的处理程序方法,但它不能决定对资源采取什么行动。正如在讨论的示例中,我们使用处理的 HTTP GET 方法来获取数据。

一旦我们知道了如何获取数据,接下来让我们通过以下步骤更新数据:

  1. 在 MyBookController 中添加 updateBook() 方法,它将被 @PutMapping 注解标记,以处理如下 URL:
        @PutMapping("/books/{ISBN}") 
          public ResponseEntity<Book> updateBook(@PathVariable long  
          ISBN, @RequestBody Book book)  
        { 
          Book book_searched = bookDAO.getBook(ISBN); 
          if (book_searched == null) { 
            return new ResponseEntity(HttpStatus.NOT_FOUND); 
          } 
          bookDAO.updateBook(ISBN, book.getPrice()); 

          book_searched.setPrice(book.getPrice()); 
          return new ResponseEntity(book_searched, HttpStatus.OK); 
        } 

在这里,URL 被映射为 PUT 方法。

updateBook() 方法包括:

  • 该参数是 ISBN,已通过@PathVariable注解绑定其值。

  • 第二个参数是类型为 Book 并注解为@ResponseBody的对象。@ResponseBody注解是用于绑定 HTTP 响应体的标记,它用于将 HTTP 响应体绑定到领域对象。此注解使用 Spring 框架的标准 HTTP 消息转换器将响应体转换为相应的领域对象。

在这种情况下,MappingJacksonHttpMessageConverter将被选择将到达的 JSON 消息转换为 Book 对象。为了使用转换器,我们在 lib 文件夹中添加了相关库。我们将在后面的页面详细讨论消息转换器。

  1. 如下面的代码所示,更新书籍的客户端代码:
        public class Main_Update { 
          public static void main(String[] args) { 
            // TODO Auto-generated method stub 
            RestTemplate template = new RestTemplate(); 

            Map<String,Long> request_parms=new HashMap<>(); 
            request_parms.put("ISBN",13l); 

            Book book=new Book(); 
            book.setPrice(200); 
            template.put 
              ("http://localhost:8081/Ch09_Spring_Rest_JDBC/books/13", 
                book,request_parms); 
          } 
        } 

PUT 方法的签名如下:

        void put(URL_for_the_resource, Object_to_update,Map_of_variable) 

  1. 现在让我们通过 POSTMAN 进行测试,输入 URL,从下拉菜单中选择 PUT 方法,输入正文值,如下所示,然后点击发送:

获取和更新数据后,现在让我们添加以下步骤的代码以添加书籍资源:

  1. 在控制器中添加一个addBook()方法,用@PostMapping 注解。

  2. 我们将使用@RequestBody注解将 HTTP 请求体绑定到book领域对象,如下面的代码所示:

        @PostMapping("/books") 
        public ResponseEntity<Book> addBook(@RequestBody Book book) { 
          System.out.println("book added" + book.getDescription()); 
          if (book == null) { 
            return new ResponseEntity<Book>(HttpStatus.NOT_FOUND); 
          } 
          int data = bookDAO.addBook(book); 
          if (data > 0) 
            return new ResponseEntity(book, HttpStatus.OK); 
          return new ResponseEntity(book, HttpStatus.NOT_FOUND); 
        } 

@RequestBody注解将请求体绑定到领域对象,在我们这个案例中是 Book 对象。

  1. 现在让我们添加如下所示的客户端代码:
        public class Main_AddBook { 
          public static void main(String[] args) { 
            // TODO Auto-generated method stub 
            RestTemplate template = new RestTemplate(); 

            Book book=new Book("add book",1234l,"adding  
              book",1000,"description adding","abcd"); 
            book.setDescription("new description"); 
            Book book2= template.postForObject( 
              "http://localhost:8081/Ch09_Spring_Rest_JDBC/books",   
              book,Book.class); 
            System.out.println(book2.getAuthor()); 
          } 
        } 

POST 方法取资源 URL、要在资源中添加的对象以及对象类型作为参数。

  1. 在 POSTMAN 中,我们可以添加资源 URL 并选择 POST 方法,如图所示:

  1. 同样,我们将添加一个获取所有书籍的资源,如下所示:
          @GetMapping("/books") 
          public ResponseEntity getAllBooks() { 

            List<Book> books = bookDAO.findAllBooks(); 
            return new ResponseEntity(books, HttpStatus.OK); 
          } 

为了测试 getAllBook,请按照以下方式添加客户端代码:

        public class Main_GetAll { 

          public static void main(String[] args) { 
            RestTemplate template = new RestTemplate(); 
            ResponseEntity<Book[]> responseEntity=   
              template.getForEntity( 
                "http://localhost:8081/Ch09_Spring_Rest_JDBC/books",   
                Book[].class); 
            Book[] books=responseEntity.getBody(); 
            for(Book book:books) 
            System.out.println(book.getAuthor()+"\t"+book.getISBN()); 
          } 
        } 

响应是 JSON 类型,包含书籍数组,我们可以从响应体中获取。

  1. 让我们通过 POSTMAN 获取列表,通过添加 URL localhost:8081/Ch09_Spring_Rest_JDBC/books并选择 GET 方法。我们将以 JSON 格式获取书籍列表,如图快照所示:

同样,我们可以编写一个通过 ISBN 删除书籍的方法。你可以找到代码

数据展示

在讨论的示例中,我们使用 JSON 来表示资源,但在实践中,消费者可能更喜欢其他资源格式,如 XML、PDF 或 HTML。无论消费者想要哪种表示格式,控制器都最不关心。Spring 提供了以下两种方式来处理响应,将其转换为客户端将消费的表现状态。

  • HTTP based message converters

  • 基于视图的视图渲染协商。

Http-based message converters

控制器执行它们的主要任务,产生数据,这些数据将在视图部分展示。有多种方法可以识别用于表示的视图,但有一种直接的方法,其中从控制器返回的对象数据隐式转换为适合客户端的适当表示。隐式转换的工作由 HTTP 消息转换器完成。以下是由 Spring 提供的处理消息和 Java 对象之间常见转换的消息转换器:

  • ByteArrayHttpMessageConverter - 它转换字节数组

  • StringHttpMessageConverter - 它转换字符串

  • ResourceHttpMessageConverter - 它转换 org.springframework.core.io.Resource 为任何类型的字节流

  • SourceHttpMessageConverter - 它转换 javax.xml.transform.Source

  • FormHttpMessageConverter - 它转换表单数据到/自 MultiValueMap<String, String>的值。

  • Jaxb2RootElementHttpMessageConverter - 它将 Java 对象转换为/从 XML

  • MappingJackson2HttpMessageConverter - 它转换 JSON

  • MappingJacksonHttpMessageConverter - 它转换 JSON

  • AtomFeedHttpMessageConverter - 它转换 Atom 源

  • RssChannelHttpMessageConverter - 它转换 RSS 源

  • MarshallingHttpMessageConverter - 它转换 XML

基于协商视图的视图渲染

我们已经深入讨论了 Spring MVC,以处理数据并展示数据。ModelAndView 有助于设置视图名称和其中要绑定的数据。视图名称随后将由前端控制器使用,通过 ViewResolver 的帮助从确切位置定位实际视图。在 Spring MVC 中,仅解析名称并在其中绑定数据就足够了,但在 RESTful Web 服务中,我们需要比这更多。在这里,仅仅匹配视图名称是不够的,选择合适的视图也很重要。视图必须与客户端所需的代表状态相匹配。如果用户需要 JSON,则必须选择能够将获取的消息渲染为 JSON 的视图。

Spring 提供 ContentNegotiatingViewResolver 以根据客户端所需的内容类型解析视图。以下是我们需要添加以选择视图的 bean 配置:

配置中引用了 ContentNegotiationManagerFacrtoryBean,通过'cnManager'引用。我们将在讨论演示时进行其配置。在这里,我们配置了两个 ViewResolvers,一个用于 PDF 查看器,另一个用于 JSP。

从请求路径中检查的第一个事情是其扩展名以确定媒体类型。如果没有找到匹配项,则使用请求的文件名使用 FileTypeMap 获取媒体类型。如果仍然不可用,则检查接受头。一旦知道媒体类型,就需要检查是否支持视图解析器。如果可用,则将请求委派给适当的视图解析器。在开发自定义视图解析器时,我们需要遵循以下步骤:

  1. 开发自定义视图。这个自定义视图将是 AbstractPdfView 或 AbstractRssFeedView 或 AbstractExcelView 的子视图。
  • 根据视图,需要编写 ViewResolver 实现。

  • 在上下文中注册自定义视图解析器。

  • 让我们使用自定义视图解析器和示例数据逐步生成 PDF 文件。

  1. 添加 boo-servlet.xml 处理器映射文件,其中将包含注解配置和发现控制器的配置。你可以从之前的应用程序中复制这个。

  2. 在 web.xml 中添加前端控制器,就像在之前的应用程序中一样。

  3. 下载并添加 itextpdf-5.5.6.jar 以处理 PDF 文件。

  4. 创建 Ch09_Spring_Rest_ViewResolver 作为动态网络应用程序,并添加所有必需的 jar 文件。

  5. MyBookController作为 RestController 添加到 com.packt.ch09.controller 包中。处理'books/{author}' URL 的方法。该方法有一个 ModelMap 参数,用于添加'book list'模型。这里我们添加了一个书目列表的占位符,但你也可以添加从数据库获取数据的代码。代码如下所示:

        @RestController 
         public class MyBookController { 
         @RequestMapping(value="/books/{author}", method =   
           RequestMethod.GET) 
         public String getBook(@PathVariable String author,  
           ModelMap model)  
           { 
             List<Book> books=new ArrayList<>(); 
            books.add(new    
              Book("Book1",10l,"publication1",100, 
              "description","auuthor1")); 
            books.add(new Book("Book2",11l,"publication1",200,    
              "description","auuthor1")); 
            books.add(new Book("Book3",12l,"publication1",500, 
              "description","auuthor1")); 

            model.addAttribute("book", books); 
             return "book"; 
          } 
        } 

我们稍后将在'book'作为视图名称的情况下添加 JSP 视图,这是由处理器方法返回的。

  1. 让我们添加一个AbstarctPdfView的子视图 PDFView,如下所示的代码:
        public class PdfView extends AbstractPdfView { 
          @Override 
          protected void buildPdfDocument(Map<String, Object> model,  
            Document document, PdfWriter writer, 
              HttpServletRequest request, HttpServletResponse    
                response) throws Exception  
          { 
            List<Book> books = (List<Book>) model.get("book"); 
            PdfPTable table = new PdfPTable(3); 
              table.getDefaultCell().setHorizontalAlignment 
            (Element.ALIGN_CENTER); 
            table.getDefaultCell(). 
              setVerticalAlignment(Element.ALIGN_MIDDLE); 
            table.getDefaultCell().setBackgroundColor(Color.lightGray); 

            table.addCell("Book Name"); 
            table.addCell("Author Name"); 
            table.addCell("Price"); 

            for (Book book : books) { 
              table.addCell(book.getBookName()); 
              table.addCell(book.getAuthor()); 
              table.addCell("" + book.getPrice()); 
            } 
            document.add(table); 

          } 
        } 

pdfBuildDocument()方法将使用 PdfTable 帮助设计 PDF 文件的外观,作为具有表头和要显示的数据的文档。.addCell()方法将表头和数据绑定到表中。

  1. 现在让我们添加一个实现ViewResolver的 PdfViewResolver,如下所示:
        public class PdfViewResolver implements ViewResolver{ 

          @Override 
          public View resolveViewName(String viewName, Locale locale)  
            throws Exception { 
            PdfView view = new PdfView(); 
            return view; 
          } 
        } 

  1. 现在我们需要将视图解析器注册到上下文。这可以通过在配置中添加内容协商视图解析器 bean 来完成。

  2. 内容协商视图解析器 bean 引用内容协商管理器工厂 bean,因此让我们再添加一个 bean,如下所示:

        <bean id="cnManager"  class= "org.springframework.web.accept. 
            ContentNegotiationManagerFactoryBean"> 
            <property name="ignoreAcceptHeader" value="true" /> 
            <property name="defaultContentType" value="text/html" /> 
        </bean>
  1. 我们已经添加了自定义视图,但我们也将添加 JSP 页面作为我们的默认视图。让我们在/WEB-INF/views 下添加 book.jsp。你可以检查 InternalResourceViewResolver 的配置以获取 JSP 页面的确切位置。以下是用以下步骤显示的代码:
        <html> 
        <%@ taglib prefix="c"   
                 uri="http://java.sun.com/jsp/jstl/core"%> 
        <title>Book LIST</title> 
        </head> 
        <body> 
          <table border="1"> 
            <tr> 
              <td>Book NAME</td> 
              <td>Book AUTHOR</td> 
              <td>BOOK PRICE</td> 
            </tr> 
            <tr> 
              <td>${book.bookName}</td> 
              <td>${book.author}</td> 
              <td>${book.price}</td> 
            </tr> 
          </table> 
        </body> 
        </html>
  1. 是的,我们已经完成了应用程序的编写,现在该是测试应用程序的时候了。在服务器上运行应用程序,在浏览器中输入http://localhost:8080/Ch09_Spring_Rest_ViewResolver/books/author1.pdf

auuthor1是作者的名字,我们想要获取他的书籍列表,扩展名 PDF 显示消费者期望的视图类型。

在浏览器中,我们将得到以下输出:

摘要


在本章的开头,我们讨论了网络服务以及网络服务的重要性。我们还讨论了 SOAP 和 RESTful 网络服务。我们深入讨论了如何编写处理 URL 的 RestController。RestController 围绕 URL 展开,我们概述了如何设计 URL 以映射到处理方法。我们开发了一个在客户端请求到来时处理所有 CRUD 方法的 RestController,该控制器与数据库交互。我们还深入讨论了 RestTemplate,这是一种简单且复杂度较低的测试 RESTful 网络服务的方法,该方法适用于不同类型的 HTTP 方法。进一步地,我们还使用 POSTMAN 应用程序测试了开发的网络服务。无论消费者需要什么,开发网络服务都是一种单向交通。我们还探讨了消息转换器和内容协商,以通过不同的视图服务于消费者。

在下一章中,我们将探讨最具讨论性的话题,以及一个正在改变网络体验的 Spring 新入门。我们将讨论关于 WebSocket 的内容。

第九章。交换消息:消息传递

到目前为止,我们已经讨论了很多关于基于传统 HTTP 通信的双向网络应用程序。这些基于浏览器的应用程序通过打开多个连接提供双向通信。WebSocket 协议提供了一种基于 TCP 的消息传递方式,不依赖于打开多个 HTTP 连接。在本章中,我们将通过以下几点讨论 WebSocket 协议:

  • 消息传递简介

  • WebSocket 协议简介

  • WebSocket API

  • STOMP 概览

在网络应用程序中,客户端和服务器之间的双向通信是同步的,其中客户端请求资源,服务器发送 HTTP 调用通知。它解决了以下问题:

  • 必须打开多个连接以发送信息并收集传入消息

  • 跟踪将外出连接映射到进入连接,以便跟踪请求及其回复

更好的解决方案是维护一个用于发送和接收的单一 TCP 连接,这正是 WebSocket 作为无头部的低层协议所提供的。由于没有添加头部,传输网络的数据量减少,从而降低了负载。这是通过称为拉取技术的过程来实现的,而不是 AJAX 中使用的长拉取的推技术。现在,开发者们正在使用XMLHttpRequest(XHR)进行异步 HTTP 通信。WebSocket 使用 HTTP 作为传输层,以支持使用 80、443 端口的基础设施。在这种双向通信中,成功的连接数据传输是独立于他们的意愿进行的。

《RFC 6455》将 WebSocket 协议定义为,一种在客户端运行在受控环境中与远程主机通信的客户端和服务器之间的双向通信协议,该远程主机已允许接受邮件、电子邮件或任何直接从代码发出的通信。该协议包括打开握手,随后是基本的报文框架,该框架建立在 TCP 协议之上。如果服务器同意,它会发送 HTTP 状态码 101,表示成功的握手。现在连接将保持开放,可以进行消息交换。以下图表给出了通信如何进行的大致概念:

WebSocket 在 TCP 之上执行以下操作:

  • 它向浏览器添加了网络安全模型。

  • 由于一个端口需要支持多个主机名和多个服务,因此它增加了地址和命名机制以提供这种支持。

  • 它在 TCP 之上形成了一层框架机制,以促进 IP 包机制。

  • 关闭握手机制。

在 WebSocket 中的数据传输使用一系列的帧。这样的数据帧可以在打开握手之后,在端点发送 Close 帧之前,由任一方随时传输。

Spring 和消息传递


从 Spring 4.0 开始,就有对 WebSocket 的支持,引入了 spring-websocket 模块,该模块与 Java WebSocket API(JSR-356)兼容。HTTPServlet 和 REST 应用程序使用 URL、HTTP 方法在客户端和服务器之间交换数据。但与这种相反,WebSocket 应用程序可能使用单个 URL 进行初始握手,这是异步的、消息传递的,甚至是基于 JMS 或 AMQP 的架构。Spring 4 包括 spring-messaging 模块,以集成消息、消息通道、消息处理器,一系列注解用于将消息映射到方法,以及许多其他功能以支持基本的消息传递架构。@Controller 和@RestController 我们已经用来创建 Spring MVC 网络应用程序和 RESTful 网络服务,它允许处理 HTTP 请求也支持 WebSocket 消息的处理方法。此外,控制器中的处理方法可以将消息广播给所有感兴趣的用户特定的 WebSocket 客户端。

使用

WebSocket 架构适用于所有那些需要频繁交换事件的网络应用程序,在这些应用程序中,数据交换到目标的时间是非常重要的,例如:

  • 社交媒体目前在日常生活中扮演着非常重要的角色,并且对于与家人和朋友保持联系起着至关重要的作用。用户总是喜欢实时接收他们圈子内完成的 Feed 更新。

  • 如今,许多在线多人游戏都可以在网络上找到。在这样的游戏中,每个玩家总是急于知道他的对手正在做什么。没有人希望在对手采取行动时发现对手的举动。

  • 在开发过程中,版本控制工具如 Tortoise SVN、Git 有助于跟踪文件。这样,在代码交换时就不会发生冲突,变得更加容易。但在这里,我们无法实时获取谁正在处理哪个文件的信息。

  • 在金融投资中,人们总是希望知道他所感兴趣公司的实时股价,而不是之前的某个时间的股价。

WebSocket API 概述


springframework 框架通过提供采用各种 WebSocket 引擎的 API,实现了 WebSocket 的创建。如今 Tomcat7.0.47+、Jetty 9.1+、WebLogic 12.1.3+、GlassFish 4.1+为 WebSocket 提供了运行环境。

WebSocket 处理器的创建

我们可以通过实现 WebSocketHandler 接口或从 TextWebSocketHandler 或 BinaryWebSocketHandler 继承来创建 WebSocketHandler,如下代码片段所示:

public class MyWebSocketHandler extends TextWebSocketHandler{ 
@Override 
   public void handleTextMessage(WebSocketSession session,     
     TextMessage message) 
   { 
       // code goes here 
   } 
} 

可以使用 WebSocketDecorator 类来装饰 WebSocketHandler。Spring 提供了一些装饰器类来处理异常、日志机制和处理二进制数据。ExceptionWebSocketHandler是一个异常处理的 WebSocketHandlerDecorator,它可以帮助处理所有 Throwable 实例。LoggingWebSocketHandlerDecorator为 WebSocket 生命周期中发生的事件添加日志记录。

注册 WebSocketHandler

WebSocket 处理器被映射到特定的 URL 以注册此映射。此框架可以通过 Java 配置或 XML 基础配置来完成。

基于 Java 的配置

WebSocketConfigurer 用于在registerWebSocketHandlers()方法中将处理器与其特定的 URL 映射,如下面的代码所示:

@Configuration 
@EnableWebSocket 
public class WebSocketConfig implements WebSocketConfigurer { 
  @Override 
  public void registerWebSocketHandlers(WebSocketHandlerRegistry   
     registry)  
  { 
     registry.addHandler(createHandler(), "/webSocketHandler"); 
  } 
  @Bean 
  public WebSocketHandler createMyHandler() { 
    return new MyWebSocketHandler(); 
  } 
} 

在此处,我们的 WebSocketHandler 被映射到了/webSocketHandler URL。

自定义 WebSocketHandler 以自定义握手的操作可以如下进行:

@Configuration 
@EnableWebSocket 
public class MyWebSocketConfig implements WebSocketConfigurer { 
  @Override 
  public void registerWebSocketHandlers(WebSocketHandlerRegistry   
    registry)  
  { 
    registry.addHandler(createHandler(),     
       "/webSocketHandler").addInterceptors 
       (new HttpSessionHandshakeInterceptor()); 
  } 
  @Bean 
  public WebSocketHandler createMyHandler() { 
    return new MyWebSocketHandler(); 
  } 
} 

握手拦截器暴露了beforeHandshake()afterhandshake()方法,以自定义 WebSocket 握手。HttpSessionHandshakeInterceptor促进了将 HtttpSession 中的信息绑定到名为HTTP_SESSION_ID_ATTR_NAME的握手属性下。这些属性可以用作WebSocketSession.getAttributes()方法。

XML 基础配置

上述 Java 代码片段中的注册也可以用 XML 完成。我们需要在 XML 中注册 WebSocket 命名空间,然后如以下所示配置处理器:

<beans xmlns="http://www.springframework.org/schema/beans" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xmlns:websocket= 
http://www.springframework.org/schema/websocket 
   xsi:schemaLocation= 
    "http://www.springframework.org/schema/beans 
  http://www.springframework.org/schema/beans/spring-beans.xsd 
    http://www.springframework.org/schema/websocket 
http://www.springframework.org/schema/websocket/spring- 
    websocket.xsd"> 
  <websocket:handlers> 
    <websocket:mapping path="/myWebSocketHandler"  
     handler="myWebSocketHandler"/> 
     </websocket:handlers> 
   <bean id="myWebSocketHandler"   
    class="com.packt.ch10.WebsocketHandlers. MyWebSocketHandler"       
    /> 
</beans> 

在 XML 中自定义的 WebSocketConfigurer 可以写成如下形式:

<websocket:handlers> 
    <websocket:mapping path="/myWebSocketHandler"  
       handler="myWebSocketHandler"/> 
    <websocket:handshake-interceptors> 
      <bean class= 
         "org.springframework.web.socket.server.support. 
         HttpSessionHandshakeInterceptor"/> 
    </websocket:handshake-interceptors> 
     </websocket:handlers> 
  <!-bean for MyWebSocketHandler -à 
</beans>  

WebSocket 引擎配置

Tomcat7.0.47+, Jetty 9.1+,WebLogic 12.1.3+, GlassFish 4.1+ 为 WebSocket 提供运行环境。可以通过添加 WebSocketConfigurer 的 bean 来为 Tomcat 运行环境配置消息缓冲区大小、超时等特性,如下所示:

@Bean 
public ServletServerContainerFactoryBean  
  createWebSocketContainer()  
{ 
  ServletServerContainerFactoryBean webSocketcontainer =  
    new ServletServerContainerFactoryBean(); 
    webSocketcontainer .setMaxTextMessageBufferSize(9000); 
    webSocketcontainer .setMaxBinaryMessageBufferSize(9000); 
  return webSocketcontainer ; 
  } 
} 

等效的 XML 配置可以写成:

<bean class= "org.springframework.web.socket.server.standard. 
  ServletServerContainerFactoryBean"> 
  <property name="maxTextMessageBufferSize" value="9000"/> 
  <property name="maxBinaryMessageBufferSize" value="9000"/> 
</bean> 

允许的来源配置

origin是代理商的特权范围。由众多作者以各种格式创建的内容存在其中,其中一些可能是有害的。由一个来源创建的内容可以自由地与其他来源的内容进行交互。代理商有设置规则的权限,其中一个内容与其他内容交互,称为“同源策略”

让我们以 HTML 为例,其中有表单提交。每当用户代理输入数据时,输入的数据会被导出到 URI。在此处,URI 声明了对脚本文件通过 URI 接收的信息的完整性信任。

http://packt.com/, http://packt.com:8080/, http://www.packt.com/, https://packt.com:80/, https://packt.com/, http://packt.org/ 是不同的 URI。

配置来源有三种方式:

  • 允许同源

  • 允许指定的来源列表。

  • 允许所有来源

让我们首先详细讨论关于客户端服务器通信中 WebSocket 的创建和使用:

  1. WebSocket 的创建:
      WebSocket socket=  new WebSocket( URL, protocols); 

  • URL 包含的内容:

    • 协议:URL 必须包含ws,表示不安全连接,或wss,表示安全连接。

    • 主机名:这是服务器的一个名称或 IP 地址。

    • 端口:我们要连接的远程端口,ws 连接默认使用端口'80',而 wss 使用 443。

    • 资源名称:要获取的资源的路径 URL。

  • 我们可以将 WebSocket 的 URL 写为:

    • 协议://主机名:端口号/资源路径

    • ws://主机名:端口号/资源路径

    • wss://主机名:端口号/资源路径

  1. 关闭 WebSocket:

关闭连接时,我们使用close()方法,如close(code, reason)

注意

代码:这是一个发送给服务器的数值状态。1000 表示正常关闭连接。

  1. WebSocket 的状态:

以下是 WebSocket 的连接状态,提供它处于哪种状态的信息:

  • 连接中:构造 WebSocket,并尝试连接到指定 URL。这个状态被认为是连接状态,准备状态值为 0。

  • 打开:一旦 WebSocket 成功连接到 URL,它将进入打开状态。只有在 WebSocket 处于打开状态时,数据才能在网络之间发送和接收。打开状态的准备状态值是"1"。

  • 关闭:WebSocket 不会直接关闭,它必须与服务器通信,通知它正在断开连接。这个状态被认为是关闭状态。"open"状态的准备状态值是"2"。

  • 已关闭:从服务器成功断开连接后,WebSocket 进入关闭状态。处于关闭状态的 WebSocket 有一个"readyState"值为 3。

  1. 在 WebSocket 中的事件处理:

WebSocket 基于事件处理原理工作,其中回调方法被调用以完成过程。以下是 WebSocket 生命周期中发生的事件:

  • onopen:当 WebSocket 过渡到开放状态时,"onopen"事件处理程序会被调用。

  • onmessage:当 WebSocket 从服务器接收数据时,"onmessage"事件处理程序会被调用。接收到的数据将存储在"message"事件的"data"字段中。

数据字段有参数:

  • onclose:当 WebSocket 关闭时,"onclose"事件处理程序会被调用。事件对象将传递给"onclose"。它有三个字段:

  • 代码:服务器提供的数值状态值。

  • 原因:这是一个描述关闭事件的字符串。

  • wasClean:有一个布尔值,表示连接是否没有问题地关闭。在正常情况下,"wasClean"是 true。

  • onerror:当 WebSocket 遇到任何问题时,"onerror"事件处理程序会被调用。传递给处理程序的事件将是一个标准错误对象,包括"name"和"message"字段。

  1. 发送数据:

数据传输通过send()方法进行,该方法处理 UTF-8 文本数据、ArrayBuffer 类型的数据以及 blob 类型的数据。'bufferedAmount'属性值为零确保数据发送成功。

让我们通过以下步骤开发一个 WebSocket 演示来查找国家首都:

  1. 创建 Ch10_Spring_Message_Handler 作为动态网络应用程序。

  2. 添加 Spring 核心、Spring 网络、spring-websocket、spring-messaging 模块的 jar 文件。还要添加 Jackson 的 jar 文件。

  3. 让我们在 compackt.ch10.config 包中添加 MyMessageHandler 作为 TextWebSocketHandler 的子项。覆盖处理消息、WebSocket 连接、连接关闭的方法,如下所示:

public class MyMessageHandler extends TextWebSocketHandler { 

        List<WebSocketSession> sessions = new CopyOnWriteArrayList<>(); 

          @Override 
          public void handleTextMessage(WebSocketSession session,  
            TextMessage message) throws IOException { 
            String country = message.getPayload(); 
            String reply="No data available"; 
            if(country.equals("India"))  { 
              reply="DELHI"; 
            } 
            else if(country.equals("USA"))  { 
                  reply="Washington,D.C";     
             } 
            System.out.println("hanlding message"); 

            for(WebSocketSession webSsession:sessions){ 
              session.sendMessage(new TextMessage(reply));   
            } 
          } 
          @Override 
          public void afterConnectionEstablished(WebSocketSession  
             session) throws IOException { 
            // Handle new connection here 
            System.out.println("connection establieshed:hello"); 
            sessions.add(session); 
            session.sendMessage(new TextMessage("connection  
              establieshed:hello")); 
            } 
          @Override 
          public void afterConnectionClosed(WebSocketSession session,   
            CloseStatus status) throws IOException { 
            // Handle closing connection here 
            System.out.println("connection closed : BYE"); 
          } 
          @Override 
          public void handleTransportError(WebSocketSession session,  
            Throwable exception) throws IOException { 
              session.sendMessage(new TextMessage("Error!!!!!!")); 
            } 
        } 

这个 MessageHandler 需要注册到 WebSocketConfigurer,为所有源的 URL'/myHandler',如下所示:

        @Configuration 
        @EnableWebSocket 
        public class MyWebSocketConfigurer extends  
        WebMvcConfigurerAdapter implements WebSocketConfigurer
        { 
          @Override 
          public void
          registerWebSocketHandlers(WebSocketHandlerRegistry  
            registry) { 
            registry.addHandler(myHandler(),  
            "/myHandler").setAllowedOrigins("*"); 
          } 
          @Bean 
          public WebSocketHandler myHandler() { 
            return new MyMessageHandler(); 
          } 
          // Allow the HTML files through the default Servlet 
          @Override 
           public void configureDefaultServletHandling 
             (DefaultServletHandlerConfigurer configurer) { 
            configurer.enable(); 
          } 
        } 

  1. 在 web.xml 中添加前端控制器映射,就像在之前的应用程序中一样,servlet 名称是'books'。

  2. 为了添加viewResolver的 bean,请添加 books-servlet.xml 文件。你可以根据应用程序的需求决定是否添加它作为一个 bean。

  3. 还要添加配置以启用 Spring Web MVC,如下所示:

        <mvc:annotation-driven /> 

  1. 添加 country.jsp 作为一个 JSP 页面,其中包含一个国家列表,用户可以从下拉列表中选择国家以获取其首都名称:
        <div> 
          <select id="country"> 
                <option value="India">INDIA</option> 
                <option value="USA">U.S.A</option> 
          </select><br> 
          <br> <br> 
           <button id="show" onclick="connect();">Connect</button> 
              <br /> <br /> 
            </div> 
          <div id="messageDiv"> 
              <p>CAPITAL WILL BE DISPLAYED HERE</p> 
              <p id="msgResponse"></p> 
          </div> 
        </div> 

  1. 通过在你的资源中添加 sockjs-0.3.4.js,或者通过添加以下代码来添加 SockJS 支持:
        <script type="text/javascript"  
          src="img/sockjs-0.3.4.js"></script>
  1. 在表单提交时,会调用一个 JavaScript 方法,我们在前面讨论过的 onopen、onmessage 等 WebSocket 事件上处理该方法。
        <script type="text/javascript"> 
          var stompClient = null; 
          function setConnected(connected) { 
            document.getElementById('show').disabled = connected; 
          } 
          function connect() { 
            if (window.WebSocket) { 
              message = "supported"; 
              console.log("BROWSER SUPPORTED"); 
            } else { 
              console.log("BROWSER NOT SUPPORTED"); 
            } 
            var country = document.getElementById('country').value; 
            var socket = new WebSocket( 
              "ws://localhost:8081/Ch10_Spring_Message_Handler 
              /webS/myHandler"); 
                socket.onmessage=function(data){ 
                  showResult("Message Arrived"+data.data)        
                }; 
                setConnected(true); 
                socket.onopen = function(e) { 
                    console.log("Connection established!"); 
                    socket.send(country); 
                    console.log("sending data"); 
                };     
          } 
          function disconnect() { 
              if (socket != null) { 
                socket.close(); 
              } 
              setConnected(false); 
              console.log("Disconnected"); 
          } 
          function showResult(message) { 
            var response = document.getElementById('messageDiv'); 
            var p = document.createElement('p'); 
            p.style.wordWrap = 'break-word'; 
            p.appendChild(document.createTextNode(message)); 
            response.appendChild(p); 
          } 
        </script>

我们已经讨论过如何编写 WebSocket URL 和事件处理机制。

部署应用程序并访问页面。从下拉列表中选择国家,然后点击显示首都按钮。将显示首都名称的消息。

以下图表显示了应用程序的流程:

我们添加了控制台日志以及警告消息,以了解进度和消息的往返。根据需求,你可以自定义它,也可以完全省略。

在之前的示例中,我们使用了 WebSocket 进行通信,但其支持仍然有限。SockJS 是一个 JavaScript 库,它提供了类似于 WebSocket 的对象。

SockJS


SockJS 库提供跨浏览器、JavaScript API,以实现浏览器和服务器之间的低延迟、跨域通信。它旨在支持以下目标:

  • 使用 SockJS 实例,而不是 WebSocket 实例。

  • 这些 API 对于服务器和客户端的 API 来说都非常接近 WebSocket API。

  • 支持更快的通信

  • 客户端的 JavaScript

  • 它带有支持跨域通信的一些选择性协议

以下代码显示了如何为 WebSocketConfigurer 启用 SockJS 支持:

@Override 
public void registerWebSocketHandlers(WebSocketHandlerRegistry  
  registry)  
{ 
  registry.addHandler(myHandler(),  
    "/myHandler_sockjs").setAllowedOrigins("*").withSockJS(); 
} 

或者我们可以在 XML 中配置:

<websocket:handlers> 
   <websocket:mapping path="/myHandler"  
     handler="myHandler_sockjs"/> 
   <websocket:sockjs/> 
</websocket:handlers> 

我们可以将前面开发的 Capital 演示更新以支持 SockJS,如下所示:

  1. 在 WebContent 中添加 country_sockjs.jsp,以便与 SockJS 一起使用,如下所示:
        var socket = new SockJS( 
              "http://localhost:8080/Ch10_Spring_Message_Handler 
        /webS/myHandler_sockjs"); 

  1. 在 com.packt.ch10.config 包中添加 MyWebSocketConfigurer_sockjs 以配置 WebSocket,就像我们之前做的那样。为了启用 SockJS 支持,我们必须修改registerWebSocketHandlers()方法,像上面配置中显示的那样使用withSockJS()

  2. 运行应用程序并请求 country_sockjs.jsp 以使用 SockJS。你也可以观察控制台日志。

在上述示例中,我们使用了 WebSocket 来获取连接并处理事件。这里还引入了新的 WebSocket 协议用于通信。它使用更少的带宽。它没有 HTTP 那样的头部,使得通信更简单、高效。我们也可以使用 STOMP 进行通信。

STOMP


简单(或流式)文本导向消息协议(STOMP)通过 WebSocket 为 STOMP 帧到 JavaScript 对象的直接映射提供了支持。WebSocket 是最快的协议,但仍然不被所有浏览器支持。浏览器在支持代理和协议处理方面存在问题。所有浏览器广泛支持还需要一段时间,与此同时我们需要找到一些替代方案或实时解决方案。SockJS 支持 STOMP 协议,通过脚本语言与任何消息代理进行通信,是 AMQP 的一个替代方案。STOMP 在客户端和服务器端都很容易实现,并且提供了可靠地发送单条消息的功能,然后断开连接或从目的地消费所有消息。它定义了以下不同的帧,这些帧映射到 WebSocket 帧:

  • CONNECT(连接客户端和服务器)

  • SUBSCRIBE(用于注册,可以监听给定目的地)

  • UNSUBSCRIBE(用于移除现有订阅)

  • SEND(发送给服务器的消息):该帧将消息发送到目的地。

  • MESSAGE(来自服务器的消息):它将来自订阅的消息传递给客户端。

  • BEGIN(开始事务)

  • COMMIT(提交进行中的事务)

  • ABORT(回滚进行中的事务)

  • DISCONNECT(使客户端与服务器断开连接)

它还支持以下标准头:

  • 内容长度(content-length):SEND、MESSAGE 和 ERROR 帧包含内容长度头,其值为消息体的内容长度。

  • 内容类型(content-type):SEND、MESSAGE 和 ERROR 帧包含内容类型。它在 Web 技术中类似于 MIME 类型。

  • 收据(receipt):CONNECT 帧可能包含收据作为头属性,以确认服务器收到 RECEIPT 帧。

  • 心跳(heart-beat):它由 CONNECT 和 CONNECTED 帧添加。它包含两个由逗号分隔的正整数值。

  • 第一个值代表外出心跳。'0'指定它不能发送心跳。

  • 第二个值表示进入心跳。'0'表示不愿意接收心跳。

Spring STOMP 支持

Spring WebSocket 应用程序作为 STOMP 代理对所有客户端工作。每个消息将通过 Spring 控制器进行路由。这些控制器通过@RequestMapping 注解处理 HTTP 请求和响应。同样,它们也通过@Messaging 注解处理 WebSocket 消息。Spring 还提供了将 RabbitMQ、ActiveMQ 作为 STOMP 代理以进行消息广播的集成。

让我们逐步开发一个使用 STOMP 的应用程序:

  1. 创建 Ch10_Spring_Messaging_STOMP 作为一个动态网络应用程序,并添加我们之前添加的 jar 文件。

  2. 在 web.xml 中为 DispatcherServlet 添加映射,其名称为 books,URL 模式为'webS'。

  3. 添加 books-servlet.xml 以注册viewResolverbean。注册以发现控制器,并考虑所有 MVC 注解。

  4. 在 com.packt.ch10.config 包中添加 WebSocketConfig_custom 作为一个类,以将'/book'作为 SockJS 的端点,将'/topic'作为'/bookApp'前缀的 SimpleBroker。代码如下:

        @Configuration 
        @EnableWebSocketMessageBroker 
        public class WebSocketConfig_custom extends 
          AbstractWebSocketMessageBrokerConfigurer { 
          @Override 
          public void configureMessageBroker(
            MessageBrokerRegistry config) { 
            config.enableSimpleBroker("/topic"); 
            config.setApplicationDestinationPrefixes("/bookApp"); 
          } 
          @Override 
          public void registerStompEndpoints(
            StompEndpointRegistry registry) { 
            registry.addEndpoint("/book").withSockJS(); 
          } 
        } 

@EnableWebSocketMessageBroker使类能够作为消息代理。

  1. 在 com.packt.ch10.model 包中添加具有 bookName 作为数据成员的 MyBook POJO。

  2. 类似地,添加一个结果为数据成员的 Result POJO,其具有 getOffer 方法如下:

        public void getOffer(String bookName) { 
          if (bookName.equals("Spring 5.0")) { 
            result = bookName + " is having offer of having 20% off"; 
            } else if (bookName.equals("Core JAVA")) { 
              result = bookName + " Buy two books and get 10% off"; 
            } else if (bookName.equals("Spring 4.0")) { 
              result = bookName + " is having for 1000 till month  
            end"; 
            } 
            else 
              result = bookName + " is not available on the list"; 
          } 

  1. 添加 index.html 以从控制器获取'bookPage'链接如下:
        <body> 
               <a href="webS/bookPage">CLICK to get BOOK Page</a> 
        </body>
  1. 在 com.packt.ch10.controller 包中添加 WebSocketController 类,并用@Controller("webs")注解它。

  2. 添加注解为@RequestMapping 的bookPage()方法,以将 bookPage.jsp 发送给客户端,如下所示:

        @Controller("/webS") 
        public class WebSocketController { 
          @RequestMapping("/bookPage") 
          public String bookPage() { 
            System.out.println("hello"); 
            return "book"; 
        } 

  1. 在 jsps 文件夹中添加 bookPage.jsp。该页面将显示获取相关优惠的书籍名称。代码如下:
        <body> 
        <div> 
           <div> 
              <button id="connect" 
                onclick="connect();">Connect</button> 
              <button id="disconnect" disabled="disabled"   
                 onclick="disconnect();">Disconnect</button><br/><br/> 
            </div> 
            <div id="bookDiv"> 
                <label>SELECT BOOK NAME</label> 
                 <select id="bookName" name="bookName"> 
                     <option> Core JAVA </option>     
                     <option> Spring 5.0 </option> 
                     <option> Spring 4.0 </option> 
                 </select> 
                <button id="sendBook" onclick="sendBook();">Send to                 Add</button> 
                <p id="bookResponse"></p> 
            </div> 
          </div> 
        </body>
  1. 一旦客户端点击按钮,我们将处理回调方法,并添加 sockjs 和 STOMP 的脚本如下:
        <script type="text/javascript"                 
         src="img/sockjs-0.3.4.js"></script>            <script type="text/javascript"  
         src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/ 
        stomp.js"/> 

  1. 现在我们将逐一添加连接、断开连接、发送、订阅的方法。让我们首先添加如下获取 STOMP 连接的连接方法:
        <script type="text/javascript"> 
           var stompClient = null;  
           function connect() { 
             alert("connection"); 
           if (window.WebSocket){ 
             message="supported"; 
             console.log("BROWSER SUPPORTED"); 
           } else { 
             console.log("BROWSER NOT SUPPORTED"); 
           }                  
           alert(message); 
           var socket = new SockJS('book'); 
           stompClient = Stomp.over(socket); 
           stompClient.connect({}, function(frame) { 
           alert("in client"); 
           setConnected(true); 
           console.log('Connected: ' + frame); 
           stompClient.subscribe('/topic/showOffer',   
             function(bookResult){ 
             alert("subscribing"); 
            showResult(JSON.parse(bookResult.body).result);}); 
          }); 
        } 

连接方法创建了一个 SockJS 对象,并使用Stomp.over()为 STOMP 协议添加支持。连接添加了subscribe()来订阅'topic/showOffer'处理器的消息。我们在 WebSocketConfig_custom 类中添加了'/topic'作为 SimpleBroker。我们正在处理、发送和接收 JSON 对象。由 Result JSON 对象接收的优惠将以result: value_of_offer的形式出现。

  1. 添加断开连接的方法如下:
        function disconnect() { 
            stompClient.disconnect(); 
            setConnected(false); 
            console.log("Disconnected"); 
        } 

  1. 添加 sendBook 以发送获取优惠的请求如下:
        function sendBook()  
        { 
          var bookName =  
          document.getElementById('bookName').value; 
          stompClient.send("/bookApp/book", {},   
            JSON.stringify({ 'bookName': bookName })); 
        } 

send()向处理程序/bookApp/book发送请求,该处理程序将接受具有bookName数据成员的 JSON 对象。我们注册了目的地前缀为'bookApp',我们在发送请求时使用它。

  1. 添加显示优惠的方法如下:
        function showResult(message) { 
           //similar to country.jsp 
        } 

  1. 现在让我们在控制器中为'/book'添加处理程序方法。此方法将以下面所示的方式注解为@SendTo("/topic/showOffer'
        @MessageMapping("/book") 
          @SendTo("/topic/showOffer") 
          public Result showOffer(MyBook myBook) throws Exception { 
            Result result = new Result(); 
            result.getOffer(myBook.getBookName()); 
            return result; 
        } 

  1. 部署应用程序。然后点击链接获取优惠页面。

  2. 点击“连接”以获取服务器连接。选择书籍以了解优惠并点击发送。与书籍相关的优惠将显示出来。

以下图表解释了应用程序流程:

在控制台上,日志将以下面的形式显示,展示了 STOMP 的不同帧:

摘要


在本章中,我们深入讨论了使用 WebSocket 进行消息传递。我们概述了 WebSocket 的重要性以及它与传统网络应用程序以及基于 XMLHttpRequest 的 AJAX 应用程序的区别。我们讨论了 WebSocket 可以发挥重要作用的领域。Spring 提供了与 WebSocket 一起工作的 API。我们看到了 WebSocketHandler、WebSocketConfigurer 以及它们的使用,既使用了 Java 类,也使用了基于 XML 的配置,这些都使用国家首都应用程序来完成。SockJS 库提供了跨浏览器、JavaScript API,以实现浏览器和服务器之间低延迟、跨域通信。我们在 XML 和 Java 配置中都启用了 SockJS。我们还深入了解了 STOMP,它是用于 SockJS 上的 WebSocket 以及如何启用它及其事件处理方法。

在下一章节中,我们将探索反应式网络编程。

如果您对这本电子书有任何反馈,或者我们在未覆盖的方面遇到了困难,请在调查链接处告诉我们。

如果您有任何疑虑,您还可以通过以下方式与我们联系:

customercare@packtpub.com

我们会在准备好时发送给您下一章节........!

希望您喜欢我们呈现的内容。

posted @ 2024-05-24 10:54  绝不原创的飞龙  阅读(15)  评论(0编辑  收藏  举报