Spring5-高性能实用指南-全-

Spring5 高性能实用指南(全)

原文:zh.annas-archive.org/md5/40194AF6586468BFD8652280B650BA1F

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

这本书的使命是向开发人员介绍应用程序监控和性能调优,以创建高性能的应用程序。该书从 Spring Framework 的基本细节开始,包括各种 Spring 模块和项目、Spring bean 和 BeanFactory 实现,以及面向方面的编程。它还探讨了 Spring Framework 作为 IoC bean 容器。我们将讨论 Spring MVC,这是一个常用的 Spring 模块,用于详细构建用户界面,包括 Spring Security 身份验证部分和无状态 API。这本书还强调了构建与关系数据库交互的优化 Spring 应用程序的重要性。然后,我们将通过一些高级的访问数据库的方式,使用对象关系映射(ORM)框架,如 Hibernate。该书继续介绍了 Spring Boot 和反应式编程等新 Spring 功能的细节,并提出了最佳实践建议。该书的一个重要方面是它专注于构建高性能的应用程序。该书的后半部分包括应用程序监控、性能优化、JVM 内部和垃圾收集优化的细节。最后,解释了如何构建微服务,以帮助您了解在该过程中面临的挑战以及如何监视其性能。

第五章《理解 Spring 数据库交互》帮助我们了解 Spring Framework 与数据库交互。然后介绍了 Spring 事务管理和最佳连接池配置。最后,介绍了数据库设计的最佳实践。

这本书适合想要构建高性能应用程序并在生产和开发中更多地控制应用程序性能的 Spring 开发人员。这本书要求开发人员对 Java、Maven 和 Eclipse 有一定的了解。

这本书涵盖了什么

第一章《探索 Spring 概念》侧重于清晰理解 Spring Framework 的核心特性。它简要概述了 Spring 模块,并探讨了不同 Spring 项目的集成,并清晰解释了 Spring IoC 容器。最后,介绍了 Spring 5.0 的新功能。

第二章《Spring 最佳实践和 Bean 装配配置》探讨了使用 Java、XML 和注解进行不同的 bean 装配配置。该章还帮助我们了解在 bean 装配配置方面的不同最佳实践。它还帮助我们了解不同配置的性能评估,以及依赖注入的陷阱。

第三章《调优面向方面的编程》探讨了 Spring 面向方面的编程(AOP)模块及其各种术语的概念。它还涵盖了代理的概念。最后,介绍了使用 Spring AOP 模块实现质量和性能的最佳实践。

第四章《Spring MVC 优化》首先清楚地介绍了 Spring MVC 模块以及不同的 Spring MVC 配置方法。它还涵盖了 Spring 中的异步处理概念。然后解释了 Spring Security 配置和无状态 API 的身份验证部分。最后,介绍了 Tomcat 与 JMX 的监控部分,以及 Spring MVC 性能改进。

这本书适合谁

第六章《Hibernate 性能调优和缓存》描述了使用 ORM 框架(如 Hibernate)访问数据库的一些高级方式。最后,解释了如何使用 Spring Data 消除实现数据访问对象(DAO)接口的样板代码。

第七章,优化 Spring 消息传递,首先探讨了 Spring 消息传递的概念及其优势。然后详细介绍了在 Spring 应用程序中使用 RabbitMQ 进行消息传递的配置。最后,描述了提高性能和可伸缩性以最大化吞吐量的参数。

第八章,多线程和并发编程,介绍了 Java 线程的核心概念和高级线程支持。还介绍了 Java 线程池的概念以提高性能。最后,将探讨使用线程进行 Spring 事务管理以及编程线程的各种最佳实践。

第九章,性能分析和日志记录,专注于性能分析和日志记录的概念。本章首先定义了性能分析和日志记录以及它们如何有助于评估应用程序的性能。在本章的后半部分,重点将是学习可以用于研究应用程序性能的软件工具。

第十章,应用性能优化,专注于优化应用程序性能。还介绍了识别性能问题症状、性能调优生命周期和 Spring 中的 JMX 支持的详细信息。

第十一章,JVM 内部,介绍了 JVM 的内部结构和调优 JVM 以实现高性能的内容。还涵盖了与内存泄漏相关的主题以及与垃圾回收相关的常见误解,然后讨论了不同的垃圾回收方法及其重要性。

第十二章,Spring Boot 微服务性能调优,介绍了 Spring Boot 微服务及其性能调优的概念。还清楚地描述了如何使用执行器和健康检查来监视 Spring Boot 应用程序。还涵盖了不同的技术,以调优 Spring Boot 应用程序的性能。

充分利用本书

本书要求开发人员对 Java、Maven 和 Eclipse 有一定的了解。

下载示例代码文件

您可以从www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,可以访问www.packtpub.com/support并注册,文件将直接发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. 登录或注册www.packtpub.com

  2. 选择“支持”选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

下载文件后,请确保使用最新版本的解压缩软件解压文件夹:

  • WinRAR/7-Zip 适用于 Windows

  • Zipeg/iZip/UnRarX 适用于 Mac

  • 7-Zip/PeaZip 适用于 Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-High-Performance-with-Spring-5。如果代码有更新,将在现有的 GitHub 存储库中更新。

我们还有其他代码包,来自我们丰富的图书和视频目录,可在github.com/PacktPublishing/上找到。快去看看吧!

下载彩色图片

我们还提供了一份 PDF 文件,其中包含本书中使用的截图/图表的彩色图片。您可以从www.packtpub.com/sites/default/files/downloads/HandsOnHighPerformancewithSpring5_ColorImages.pdf.下载。

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码单词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄。这是一个例子:“为了避免LazyInitializationException,解决方案之一是在视图中打开会话。”

一块代码设置如下:

PreparedStatement st = null;
try {
    st = conn.prepareStatement(INSERT_ACCOUNT_QUERY);
    st.setString(1, bean.getAccountName());
    st.setInt(2, bean.getAccountNumber());
    st.execute();
}

当我们希望引起您对代码块的特定部分的注意时,相关的行或项目以粗体显示:

@Configuration
@EnableTransactionManagement
@PropertySource({ "classpath:persistence-hibernate.properties" })
@ComponentScan({ "com.packt.springhighperformance.ch6.bankingapp" })
    @EnableJpaRepositories(basePackages = "com.packt.springhighperformance.ch6.bankingapp.repository")
public class PersistenceJPAConfig {

}

任何命令行输入或输出都是这样写的:

curl -sL --connect-timeout 1 -i http://localhost:8080/authentication-cache/secure/login -H "Authorization: Basic Y3VzdDAwMTpUZXN0QDEyMw=="

粗体:表示新术语,重要单词,或者您在屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。这是一个例子:“在应用程序窗口内,我们可以看到一个用于本地节点的菜单。”

警告或重要提示看起来像这样。

提示和技巧看起来像这样。

第一章:探索 Spring 概念

Spring Framework 提供了广泛的支持,用于管理大型企业 Java 应用程序,并解决企业应用程序开发的复杂性。Spring 为现代企业应用程序提供了完整的 API 和配置模型,因此程序员只需专注于应用程序的业务逻辑。

Spring Framework 作为一个轻量级框架,旨在提供一种简化 Java 企业应用程序开发的方式。

本章将帮助您更好地了解 Spring Framework 的核心特性。我们将从介绍 Spring Framework 开始。本章还将让您清楚地了解 Spring Framework 的每个主要模块。在快速了解 Spring Framework 中的重要模块之后,我们将深入了解 Spring 项目的世界。我们还将清楚地了解 Spring 的控制反转(IoC)容器。最后,我们将看一下 Spring 5.0 中引入的新功能和增强功能。

在本章中,我们将讨论以下主题:

  • 介绍 Spring Framework

  • 理解 Spring 模块

  • Spring 项目

  • Spring IoC 容器

  • Spring Framework 5.0 中的新功能

介绍 Spring Framework

Spring Framework 是最受欢迎的开源 Java 应用程序框架和 IoC 容器之一。Spring 最初由 Rod Johnson 和 Jurgen Holler 开发。Spring Framework 的第一个里程碑版本于 2004 年 3 月发布。尽管已经过去了十五年,Spring Framework 仍然是构建任何 Java 应用程序的首选框架。

Spring 框架为开发企业级 Java 应用程序提供了全面的基础设施支持。因此,开发人员不需要担心应用程序的基础设施;他们可以专注于应用程序的业务逻辑,而不是处理应用程序的配置。

Spring Framework 处理所有基础设施、配置和元配置文件,无论是基于 Java 还是基于 XML。因此,这个框架为您提供了更多的灵活性,可以使用普通的 Java 对象(POJO)编程模型而不是侵入式编程模型来构建应用程序。

Spring IoC 容器通过整合应用程序的各种组件来构建整个框架的核心。Spring 的 Model-View-Controller(MVC)组件可用于构建非常灵活的 Web 层。IoC 容器简化了使用 POJOs 开发业务层。

EJB 的问题

在早期,程序员很难管理企业应用程序,因为企业 Java 技术如 Enterprise JavaBeans(EJB)对程序员提供企业解决方案的负担很重。

当 EJB 技术首次宣布时,它提供了一个分布式组件模型,允许开发人员只关注系统的业务方面,而忽略中间件的要求,如组件的连接、事务管理、持久性操作、安全性、资源池、线程、分发、远程等等;然而,开发、单元测试和部署 EJB 应用程序是一个非常繁琐的过程。在使用 EJB 时,面临以下一些复杂性:

  • 强制实现不必要的接口和方法

  • 使单元测试变得困难,特别是在 EJB 容器之外

  • 管理部署描述符中的不便之处

  • 繁琐的异常处理

当时,Spring 被引入作为 EJB 的一种替代技术,因为与其他现有的 Java 技术相比,Spring 提供了非常简单、更精简和更轻量级的编程模型。Spring 使得克服之前的复杂性成为可能,并且通过使用许多可用的设计模式,避免了使用其他更重的企业技术。Spring 框架专注于 POJO 编程模型而不是侵入式编程模型。这个模型为 Spring 框架提供了简单性。它还赋予了诸如依赖注入DI)模式和面向切面编程AOP)等概念,使用代理模式和装饰器模式。

使用 POJO 简化实现

POJO 编程模型最重要的优势是应用类的编码非常快速和简单。这是因为类不需要依赖于任何特定的 API,实现任何特殊的接口,或者扩展特定的框架类。直到真正需要它们之前,您不必创建任何特殊的回调方法。

Spring 框架的好处

Spring 框架的重要好处如下:

  • 无需重新发明轮子

  • 易于单元测试

  • 减少实现代码

  • 控制反转和 API

  • 事务管理的一致性

  • 模块化架构

  • 与时俱进

让我们详细讨论每一个。

无需重新发明轮子

无需重新发明轮子是开发人员可以从 Spring 框架中获得的最重要的好处之一。它促进了众所周知的技术、ORM 框架、日志框架、JEE、JDK 定时器、Quartz 等的实际使用。因此,开发人员不需要学习任何新的技术或框架。

它促进了良好的编程实践,例如使用接口而不是类进行编程。Spring 使开发人员能够使用 POJO 和Plain Old Java InterfacePOJI)模型编程开发企业应用程序。

易于单元测试

如果您想测试使用 Spring 开发的应用程序,这是相当容易的。这背后的主要原因是这个框架中有环境相关的代码。早期版本的 EJB 非常难以进行单元测试。甚至在容器外运行 EJB(截至 2.1 版本)都很困难。测试它们的唯一方法是将它们部署到容器中。

Spring 框架引入了 DI 概念。我们将在第二章中详细讨论 DI,Spring 最佳实践和 Bean 布线配置。DI 使得单元测试成为可能。这是通过用它们的模拟替换依赖项来完成的。整个应用程序不需要部署进行单元测试。

单元测试有多个好处:

  • 提高程序员的生产力

  • 在较早的阶段检测缺陷,从而节省修复它们的成本

  • 通过在持续集成CI)构建中自动化单元测试来预防未来的缺陷

减少实现代码

所有应用程序类都是简单的 POJO 类;Spring 不是侵入式的。对于大多数用例,它不需要您扩展框架类或实现框架接口。Spring 应用程序不需要 Jakarta EE 应用服务器,但可以部署在其中。

在 Spring 框架之前,典型的 J2EE 应用程序包含了大量的管道代码。例如:

  • 获取数据库连接的代码

  • 处理异常的代码

  • 事务管理代码

  • 日志代码等等

让我们看一个使用PreparedStatement执行查询的简单示例:

PreparedStatement st = null;
try {
    st = conn.prepareStatement(INSERT_ACCOUNT_QUERY);
    st.setString(1, bean.getAccountName());
    st.setInt(2, bean.getAccountNumber());
    st.execute();
}
catch (SQLException e) {
    logger.error("Failed : " + INSERT_ACCOUNT_QUERY, e);
} finally {
    if (st != null) {
        try {
            st.close();
        } catch (SQLException e) {
            logger.log(Level.SEVERE, INSERT_ACCOUNT_QUERY, e);
        }
    }
}

在上面的示例中,有四行业务逻辑和超过 10 行的管道代码。使用 Spring 框架可以在几行代码中应用相同的逻辑,如下所示:

jdbcTemplate.update(INSERT_ACCOUNT_QUERY,
bean.getAccountName(), bean.getAccountNumber());

使用 Spring,可以将 Java 方法作为请求处理程序方法或远程方法,就像处理 servlet API 的 servlet 容器的service()方法一样,但无需处理 servlet API。它支持基于 XML 和基于注解的配置。

Spring 使您可以使用本地 Java 方法作为消息处理程序方法,而无需在应用程序中使用 Java 消息服务(JMS)API。Spring 充当应用程序对象的容器。您的对象不必担心找到并建立彼此之间的连接。Spring 还使您可以使用本地 Java 方法作为管理操作,而无需在应用程序中使用 Java 管理扩展(JMX)API。

控制反转和 API

Spring 还帮助开发人员摆脱编写单独的编译单元或单独的类加载器来处理异常的必要性。Spring 将技术相关的异常,特别是由 Java 数据库连接(JDBC)、Hibernate 或 Java 数据对象(JDO)抛出的异常转换为未经检查的一致异常。Spring 通过控制反转和 API 来实现这一神奇的功能。

此外,它使用 IoC 进行 DI,这意味着可以正常配置方面。如果要添加自己的行为,需要扩展框架的类或插入自己的类。这种架构的优势如下所示:

  • 将任务的执行与其实现解耦

  • 更容易在不同实现之间切换

  • 程序的更大模块化

  • 通过隔离组件或模拟组件,更容易测试程序

  • 依赖关系并允许组件通过合同进行通信

事务管理的一致性

Spring 还提供了对事务管理的支持,保证一致性。它提供了一种简单灵活的方式,可以为小型应用配置本地事务,也可以为大型应用使用 Java 事务 API(JTA)配置全局事务。因此,我们不需要使用任何第三方事务 API 来执行数据库事务;Spring 将通过事务管理功能来处理它。

模块化架构

Spring 提供了一个模块化架构,帮助开发人员识别要使用和要忽略的包或类。因此,以这种方式,我们可以只保留真正需要的内容。这样即使有很多包或类,也可以轻松识别和利用可用的包或类。

Spring 是一个强大的框架,解决了 Jakarta EE 中的许多常见问题。它包括支持管理业务对象并将其服务暴露给表示层组件。

Spring 实例化 bean 并将对象的依赖项注入到应用程序中,它充当 bean 的生命周期管理器。

与时俱进

当 Spring Framework 的第一个版本构建时,其主要重点是使应用程序可测试。后续版本也面临新的挑战,但 Spring Framework 设法发展并保持领先,并与提供的架构灵活性和模块保持一致。以下是一些示例:

  • Spring Framework 在 Jakarta EE 之前引入了许多抽象,以使应用程序与特定实现解耦。

  • Spring Framework 还在 Spring 3.1 中提供了透明的缓存支持

  • Jakarta EE 在 2014 年引入了 JSR-107 用于 JCache,因此在 Spring 4.1 中提供了它

Spring 参与的另一个重大发展是提供不同的 Spring 项目。Spring Framework 只是 Spring 项目中的众多项目之一。以下示例说明了 Spring Framework 如何保持与 Spring 项目的最新状态:

  • 随着架构向云和微服务发展,Spring 推出了面向云的新 Spring 项目。Spring Cloud 项目简化了微服务的开发和部署。

  • 通过 Spring 框架引入了一种新的方法来构建 Java 批处理应用程序,即 Spring Batch 项目。

在下一节中,我们将深入探讨不同的 Spring 框架模块。

了解 Spring 模块

Spring 提供了一种模块化的架构,这是 Spring 框架受欢迎的最重要原因之一。其分层架构使得可以轻松无忧地集成其他框架。这些模块提供了开发企业应用程序所需的一切。Spring 框架分为 20 个不同的模块,这些模块建立在其核心容器之上。

以下图表说明了以分层架构组织的不同 Spring 模块:

Spring 框架模块

我们将从讨论核心容器开始,然后再讨论其他模块。

核心容器

Spring 核心容器提供了 Spring 框架的核心功能,即核心、Bean、上下文和表达式语言,其详细信息如下:

Artifact Module Usage
spring-core 该模块为其他模块使用的所有实用程序提供便利,还提供了一种管理不同 bean 生命周期操作的方式。
spring-beans 该模块主要用于解耦代码依赖于实际业务逻辑,并使用 DI 和 IoC 功能消除了单例类的使用。
spring-context 该模块提供国际化和资源加载等功能,并支持 Java EE 功能,如 EJB、JMS 和远程调用。
spring-expression 该模块提供了在运行时访问 bean 属性的支持,并允许我们操纵它们。

横切关注点

横切关注点适用于应用程序的所有层,包括日志记录和安全性等。与横切关注点相关的重要 Spring 模块如下:

Artifact Module Usage
spring-aop 该模块主要用于执行系统中各个部分共同的任务,如事务管理、日志记录和安全性。为了实现这一点,我们可以实现方法拦截器和切入点。
spring-aspects 该模块用于集成任何自定义对象类型。使用 AspectJ 是可能的,该模块的主要用途是集成容器无法控制的对象。
spring-instrument 该模块用于测量应用程序的性能,并使用跟踪信息进行错误诊断。
spring-test 该模块用于在 Spring 应用程序中集成测试支持。

数据访问/集成

数据访问/集成层在应用程序中与数据库和/或外部接口交互。它包括 JDBC、ORM、OXM、JMS 和事务模块。这些模块是spring-jdbcspring-ormspring-oxmspring-jmsspring-tx

Web

Web 层包含 Web、Web-MVC、Web-Socket 和其他 Web-Portlet 模块。各自的模块名称为spring-webspring-webmvcspring-websocketspring-webmvc-portlet

在下一节中,我们将介绍不同类型的 Spring 项目。

Spring 项目

Spring 框架为不同的基础设施需求提供了不同类型的项目,并帮助探索企业应用程序中的其他问题的解决方案:部署、云、大数据和安全性等。

一些重要的 Spring 项目列举如下:

  • Spring Boot

  • Spring 数据

  • Spring Batch

  • Spring Cloud

  • Spring 安全

  • Spring HATEOAS

让我们详细讨论它们。

Spring Boot

Spring Boot 支持创建独立的、生产级的、基于 Spring 的应用程序,只需运行即可。

Spring Boot 还提供了一些开箱即用的功能,通过对应用程序开发的一种主观观点:

  • 提供开发独立 Spring 应用程序的支持

  • 直接嵌入 Tomcat、Jetty 或 Undertow,无需部署 WAR 文件

  • 允许我们将配置外部化,以便在不同环境中使用相同的应用程序代码

  • 通过提供主观的起始 POM 简化 Maven 配置

  • 消除了代码生成和 XML 配置的需求

  • 提供用于生产特性的支持,如度量、健康检查和应用程序监控

我们将在第十二章中深入研究 Spring Boot,Spring Boot 微服务性能调优

Spring Data

Spring Data项目的主要目标是为访问数据和其他特殊功能提供一个易于使用和一致的基于 Spring 的模型,以操作基于 SQL 和 NoSQL 的数据存储。它还试图提供一种简单的方式来使用数据访问技术、映射-减少框架、关系和非关系数据库以及基于云的数据服务。

一些重要特性如下:

  • 提供与自定义存储库代码集成的支持

  • 通过使用存储库和对象映射抽象,通过使用存储库方法名称派生动态查询

  • 与 Spring MVC 控制器的高级集成支持

  • 对透明审计功能的高级支持,如创建者、创建日期、最后更改者和最后更改日期

  • 跨存储持久性的实验性集成支持

Spring Data 为以下数据源提供集成支持:

  • JPA

  • JDBC

  • LDAP

  • MongoDB

  • Gemfire

  • REST

  • Redis

  • Apache Cassandra

  • Apache Solr

Spring Batch

Spring Batch 有助于处理大量记录,包括日志/跟踪、事务管理、作业处理统计、作业重启、跳过和资源管理,通过提供可重用的功能。它还提供了更高级的技术服务和功能,可以使用优化和分区技术实现极高容量和高性能的批处理作业。

Spring Batch 的重要特性如下:

  • 以块的方式处理数据的能力

  • 启动、停止和重新启动作业的能力,包括在作业失败的情况下从失败点重新启动

  • 重试步骤或在失败时跳过步骤的能力

  • 基于 Web 的管理界面

Spring Cloud

可以说世界正在向云端迁移

Spring Cloud为开发人员提供了构建分布式系统中常见模式的工具。Spring Cloud 使开发人员能够快速构建服务和应用程序,实现在任何分布式环境中工作的常见模式。

Spring Cloud 中实现的一些常见模式如下:

  • 分布式配置

  • 服务注册和发现

  • 断路器

  • 负载平衡

  • 智能路由

  • 分布式消息传递

  • 全局锁

Spring Security

身份验证和授权是企业应用程序的重要部分,包括 Web 应用程序和 Web 服务。Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。Spring Security 专注于为 Java 应用程序提供声明式的身份验证和授权。

Spring Security 的重要特性如下:

  • 全面支持身份验证和授权

  • 与 Servlet API 和 Spring MVC 的集成支持良好

  • 模块支持与安全断言标记语言SAML)和轻量级目录访问协议LDAP)集成

  • 提供对常见安全攻击的支持,如跨站请求伪造CSRF)、会话固定、点击劫持等

我们将在第四章中讨论如何使用 Spring Security 保护 Web 应用程序,Spring MVC 优化

Spring HATEOAS

超媒体作为应用状态引擎HATEOAS)的主要目的是解耦服务器(服务提供者)和客户端(服务消费者)。服务器向客户端提供有关可以在资源上执行的其他可能操作的信息。

Spring HATEOAS 提供了一个 HATEOAS 实现,特别适用于使用 Spring MVC 实现的表述状态转移REST)服务。

Spring HATEOAS 具有以下重要特性:

  • 简化的链接定义,指向服务方法,使得链接更加健壮

  • 支持 JSON 和 JAXB(基于 XML)集成

  • 支持超媒体格式,如超文本应用语言HAL

在下一节中,我们将了解 Spring 的 IoC 容器的机制。

Spring 的 IoC 容器

Spring 的IoC 容器是 Spring 架构的核心模块。IoC 也被称为 DI。这是一种设计模式,它消除了代码对提供应用程序管理和测试的依赖性。在 DI 中,对象本身通过构造函数参数、工厂方法的参数或在创建或从工厂方法返回对象实例后设置的属性来描述它们与其他对象的依赖关系。

然后容器负责在创建 bean 时注入这些依赖关系。这个过程基本上是 bean 本身控制其依赖项的实例化或位置的逆过程(因此被称为 IoC),通过使用类的直接构造或机制。

Spring 框架的 IoC 容器有两个主要的基本包:org.springframework.beansorg.springframework.contextBeanFactory接口提供了一些高级配置机制,用于管理任何类型的对象。ApplicationContext包括了所有BeanFactory的功能,并且作为它的子接口。事实上,ApplicationContext也比BeanFactory更推荐,并提供了更多的支持基础设施,使得:更容易集成 Spring 的 AOP 特性和事务;消息资源处理方面的国际化和事件发布;以及应用层特定的上下文,比如用于 Web 应用程序的WebApplicationContext

接口org.springframework.context.ApplicationContext被表示为 Spring IoC 容器,它完全控制 bean 的生命周期,并负责实例化、配置和组装 bean。

容器通过扫描 bean 配置元数据来获取实例化、配置和组装的所有指令。配置元数据可以用以下方法表示:

  • 基于 XML 的配置

  • 基于注解的配置

  • 基于 Java 的配置

我们将在第二章中更详细地学习这些方法,Spring 最佳实践和 Bean 配置

以下图表代表了Spring 容器向创建完全配置的应用程序的过程的简单表示:

Spring IoC 容器

以下示例显示了基于 XML 的配置元数据的基本结构:

<?xml version="1.0" encoding="UTF-8"?>
<beans 

 xsi:schemaLocation="http://www.springframework.org/schema/beans
 http://www.springframework.org/schema/beans/spring-beans.xsd">

 <!-- All the bean configuration goes here -->
<bean id="..." class="...">

</bean>

<!-- more bean definitions go here -->

</beans>

id属性是一个字符串,用于标识单个bean定义。class属性定义了bean的类型,并使用了完全限定的class名称。id属性的值指的是协作对象。

什么是 Spring bean?

您可以将Spring bean视为由 Spring IoC 容器实例化、配置和管理的简单 Java 对象。它被称为 bean 而不是对象或组件,因为它是对框架起源的复杂和沉重的企业 JavaBeans 的替代。我们将在第二章中学习更多关于 Spring bean 实例化方法的内容,Spring 最佳实践和 bean 装配配置

实例化 Spring 容器

用于创建 bean 实例,我们首先需要通过读取配置元数据来实例化 Spring IoC 容器。在初始化 IoC 容器之后,我们可以使用 bean 名称或 ID 获取 bean 实例。

Spring 提供了两种类型的 IoC 容器实现:

  • BeanFactory

  • ApplicationContext

BeanFactory

BeanFactory容器充当最简单的容器,提供了对 DI 的基本支持,它由org.springframework.beans.factory.BeanFactory接口定义。BeanFactory负责在对象之间获取、配置和组装依赖关系。BeanFactory主要充当对象池,通过配置管理对象的创建和销毁。BeanFactory最受欢迎和有用的实现是org.springframework.context.support.ClassPathXmlApplicationContextClassPathXmlApplicationContext使用 XML 配置元数据来创建一个完全配置的应用程序。

以下示例定义了一个简单的HelloWorld应用程序,使用ClassPathXmlApplicationContextBeans.xml的内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans 

  xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean id="bankAccount" 
    class="com.packt.springhighperformance.ch1.bankingapp.BankAccount">
    <property name="accountType" value="Savings Bank Account" />
  </bean>
</beans>

前面的 XML 代码表示了bean XML 配置的内容。它配置了一个单独的bean,其中有一个带有name消息的属性。该属性有一个默认的value设置。

现在,以下 Java 类表示在前面的 XML 中配置的bean

让我们来看看HelloWorld.java

package com.packt.springhighperformance.ch1.bankingapp;

public class BankAccount {
  private String accountType;

  public void setAccountType(String accountType) {
    this.accountType = accountType;
  }

  public String getAccountType() {
    return this.accountType;
  }
}

最后,我们需要使用ClassPathXmlApplicationContext来创建HelloWorld bean,并调用创建的 Spring bean 中的方法。

Main.java如下所示:

package com.packt.springhighperformance.ch1.bankingapp;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.
support.ClassPathXmlApplicationContext;

public class Main {

  private static final Logger LOGGER = Logger.getLogger(Main.class);

  @SuppressWarnings("resource")
  public static void main(String[] args) {
    BeanFactory beanFactory = new 
    ClassPathXmlApplicationContext("Beans.xml");
    BankAccount obj = (BankAccount) beanFactory.getBean("bankAccount");
    LOGGER.info(obj.getAccountType());
  }
}

ApplicationContext

ApplicationContext容器提供了使用BeanFactory方法访问应用程序组件的支持。这包括BeanFactory的所有功能。此外,ApplicationContext还可以执行更多的企业功能,如事务、AOP、从属性文件解析文本消息以及将应用程序事件推送给感兴趣的监听器。它还具有将事件发布给已注册监听器的能力。

ApplicationContext的最常用的实现是FileSystemXmlApplicationContextClassPathXmlApplicationContextAnnotationConfigApplicationContext

Spring 还为我们提供了ApplicationContext接口的 Web-aware 实现,如下所示:

  • XmlWebApplicationContext

  • AnnotationConfigWebApplicationContext

我们可以使用这些实现中的任何一个来将 bean 加载到BeanFactory中;这取决于我们的应用程序配置文件的位置。例如,如果我们想要从文件系统中的特定位置加载我们的配置文件Beans.xml,我们可以使用FileSystemXmlApplicationContext类,该类在文件系统中的特定位置查找配置文件Beans.xml

ApplicationContext context = new
FileSystemXmlApplicationContext("E:/Spring/Beans.xml");

如果我们想要从应用程序的类路径加载我们的配置文件Beans.xml,我们可以使用 Spring 提供的ClassPathXmlApplicationContext类。这个类在类路径中的任何地方,包括 JAR 文件中,查找配置文件Beans.xml

ApplicationContext context = new
ClassPathXmlApplicationContext("Beans.xml");

如果您使用 Java 配置而不是 XML 配置,您可以使用AnnotationConfigApplicationContext

ApplicationContext context = new
AnnotationConfigApplicationContext(AppConfig.class);

加载配置文件并获取ApplicationContext之后,我们可以通过调用ApplicationContextgetBean()方法从 Spring 容器中获取 bean:

BankAccountService bankAccountService =
context.getBean(BankAccountService.class);

在下面的部分,我们将学习 Spring bean 的生命周期,以及 Spring 容器如何对 Spring bean 做出反应以创建和管理它。

Spring bean 生命周期

工厂方法设计模式被 Spring ApplicationContext用来按照给定的配置在容器中正确顺序创建 Spring bean。因此,Spring 容器负责管理 bean 的生命周期,从创建到销毁。在普通的 Java 应用程序中,使用 Java 的new关键字来实例化 bean,然后就可以使用了。一旦 bean 不再使用,就可以进行垃圾回收。但是在 Spring 容器中,bean 的生命周期更加复杂。

以下图表说明了典型 Spring bean 的生命周期:

Spring bean 生命周期

在下一节中,我们将看到 Spring Framework 5.0 的新功能。

Spring Framework 5.0 的新功能

Spring Framework 5.0是 Spring Framework 在 4.0 版本之后近四年的第一个重大升级。在这段时间内,最重要的发展之一就是 Spring Boot 项目的发展。我们将在下一节讨论 Spring Boot 2.0 的新功能。Spring Framework 5.0 最大的特点之一是响应式编程

Spring Framework 5.0 具有核心响应式编程功能和对响应式端点的支持。重要变化的列表包括以下内容:

  • 基线升级

  • 响应式编程支持

  • 核心功能升级

  • Spring Web MVC 升级

  • Spring 的新功能性 Web 框架WebFlux

  • 模块化支持

  • Kotlin 语言支持

  • 改进的测试支持

  • 弃用或废弃的功能

我们将在接下来的部分详细讨论这些变化。

基线升级

整个 Spring Framework 5.0 都有一个 JDK 8 和 Jakarta EE 7 的基线。基本上,这意味着要在 Spring Framework 5.0 上工作,Java 8 是最低要求。

Spring Framework 5.0 的一些重要的基线 Jakarta EE 7 规范如下:

  • Spring Framework 5.0 的代码基于 Java 8 的源代码级别。因此,使用推断泛型、lambda 等提高了代码的可读性。它还具有对 Java 8 特性的条件支持的代码稳定性。

  • Spring Framework 需要至少 Jakarta EE 7 API 级别才能运行任何 Spring Framework 5.0 应用程序。它需要 Servlet 3.1、Bean Validation 1.1、JPA 2.1 和 JMS 2.0。

  • 开发和部署过程完全兼容 JDK 9,具体如下:

  • 与类路径和模块路径兼容,具有稳定的自动模块名称

  • Spring Framework 的构建和测试套件也在 JDK 9 上通过,并且默认情况下可以在 JDK 8 上运行

响应式编程支持

响应式编程模型是 Spring 5.0 最令人兴奋的特性之一。Spring 5.0 框架基于响应式基础,完全是异步和非阻塞的。新的事件循环执行模型可以使用少量线程进行垂直扩展。

该框架获取了反应式流以提供在反应式组件管道中传递背压的系统。背压是一个确保消费者不会被来自不同生产者的数据压倒的概念。

虽然 Java 8 没有内置对响应式编程的支持,但有许多框架提供对响应式编程的支持:

  • Reactive Streams:语言中立的尝试定义响应式 API

  • Reactor:由 Spring Pivotal 团队提供的 Reactive Streams 的 Java 实现

  • Spring WebFlux:基于响应式编程开发 Web 应用程序;提供类似于 Spring MVC 的编程模型

核心功能升级

作为 Java 8 引入的新功能的一部分,Spring Framework 5.0 的核心已经进行了修订,提供了以下一些关键功能:

  • Java 8 反射增强包括在 Spring Framework 5.0 中高效地访问方法参数的功能。

  • 在 Spring Core 接口中提供对 Java 8 默认方法的选择性声明支持。

  • 支持@Nullable 和@NotNull 注释,以明确标记可为空参数和返回值。这消除了运行时的 NullPointerExceptions 的原因,并使我们能够在编译时处理空值。

对于日志记录方面,Spring Framework 5.0 提供了 Commons Logging Bridge 模块的开箱即用支持,命名为 spring-jcl,而不是标准的 Commons Logging。此外,这个新版本将能够检测 Log4j 2.x,Simple Logging Facade for Java(SLF4J),JUL(java.util.logging)等,无需任何额外的修改。

它还通过为 getFile 方法提供 isFile 指示符,支持 Resource 抽象。

Spring Web MVC 升级

Spring 5.0 完全支持 Spring 提供的 Filter 实现中的 Servlet 3.1 签名。它还为 Spring MVC 控制器方法中的 Servlet 4.0 PushBuilder 参数提供支持。

Spring 5.0 还通过 MediaTypeFactory 委托提供了对常见媒体类型的统一支持,包括使用 Java Activation Framework。

新的 ParsingPathMatcher 将作为 AntPathMatcher 的替代,具有更高效的解析和扩展语法。

Spring 5.0 还将提供对 ResponseStatusException 的支持,作为@ResponseStatus 的编程替代。

Spring 的新功能性 Web 框架-WebFlux

为了支持响应式 HTTP 和 WebSocket 客户端,Spring Framework 5.0 提供了 spring-webflux 模块。Spring Framework 5.0 还为在服务器上运行的响应式 Web 应用程序提供了对 REST、HTML 和 WebSocket 风格交互的支持。

在 spring-webflux 中,服务器端有两种主要的编程模型:

  • 支持@Controller 注释,包括其他 Spring MVC 注释

  • 提供对 Java 8 Lambda 的函数式风格路由和处理支持

Spring spring-webflux 还提供了对 WebClient 的支持,它是响应式和非阻塞的,作为 RestTemplate 的替代。

模块化支持

模块化框架在 Java 平台上很受欢迎。从 Java 9 开始,Java 平台变得模块化,有助于消除封装中的缺陷。

有一些问题导致了模块化支持,如下所述:

  • Java 平台大小:在过去的几十年里,Java 不需要添加模块化支持。但是市场上有许多新的轻量级平台,如物联网(IoT)和 Node.js。因此,迫切需要减小 JDK 版本的大小,因为初始版本的 JDK 大小不到 10MB,而最近的版本需要超过 200MB。

  • ClassLoader 困难:当 Java ClassLoader 搜索类时,它将选择周围的类定义,并立即加载第一个可用的类。因此,如果在不同的 JAR 中有相同的类可用,那么 ClassLoader 无法指定要加载类的 JAR。

为了使 Java 应用程序模块化,Open System Gateway initiative (OSGi)是将模块化引入 Java 平台的倡议之一。在 OSGi 中,每个模块被表示为一个 bundle。每个 bundle 都有自己的生命周期,具有不同的状态,如已安装、已启动和已停止。

Jigsaw 项目是 Java 社区流程(JCP)的主要动力,旨在将模块化引入 Java。其主要目的是为 JDK 定义和实现模块化结构,并为 Java 应用程序定义模块系统。

Kotlin 语言支持

Spring Framework 5.0 引入了静态类型的 JVM 语言支持Kotlin 语言 (kotlinlang.org/),它使得代码简短、可读且表达力强。Kotlin 基本上是一种运行在 JVM 之上的面向对象的语言,也支持函数式编程风格。

有了 Kotlin 支持,我们可以深入了解函数式 Spring 编程,特别是对于函数式 Web 端点和 bean 注册。

在 Spring Framework 5.0 中,我们可以编写干净可读的 Kotlin 代码用于 Web 功能 API,如下所示:

{
    ("/bank" and accept(TEXT_HTML)).nest {
        GET("/", bankHandler::findAllView)
        GET("/{customer}", bankHandler::findOneView)
    }
    ("/api/account" and accept(APPLICATION_JSON)).nest {
        GET("/", accountApiHandler::findAll)
        GET("/{id}", accountApiHandler::findOne)
    }
}

在 Spring 5.0 版本中,Kotlin 的空安全支持也提供了使用@NonNull@Nullable@NonNullApi@NonNullFields注解的指示,来自org.springframework.lang包。

还有一些新添加的 Kotlin 扩展,基本上是为现有的 Spring API 添加了函数扩展。例如,来自org.springframework.beans.factory包的扩展fun <T : Any> BeanFactory.getBean(): Torg.springframework.beans.factory.BeanFactory添加了支持,可以通过指定 bean 类型作为 Kotlin 的 reified 类型参数来搜索 bean,而无需类参数:

@Autowired
lateinit var beanFactory : BeanFactory

@PostConstruct
fun init() {
 val bankRepository = beanFactory.getBean<BankRepository>()

}

还可以在org.springframework.ui中找到另一个扩展,它提供了操作符重载支持,以向model接口添加类似数组的 getter 和 setter:

model["customerType"] = "Premium"

改进的测试支持

在测试方面,Spring Framework 5.0 同样支持 JUnit Jupiter (junit.org/junit5/docs/current/user-guide/)。它有助于在 JUnit 5 中编写测试和扩展。它还提供了一个测试引擎来运行基于 Jupiter 构建的测试,关于 Spring 的方面,还提供了一个编程和扩展模型。

Spring Framework 5.0 还支持 Spring TestContext Framework 中的并行测试执行。对于 Spring WebFlux,spring-test还包括对WebTestClient的支持,以整合对响应式编程模型的测试支持。

没有必要为测试场景运行服务器。通过使用新的WebTestClient,类似于MockMvcWebTestClient可以直接绑定到 WebFlux 服务器基础设施,使用模拟请求和响应。

已删除或弃用的功能

在 Spring 5.0 中,一些包已经在 API 级别被删除或弃用。spring-aspects模块的mock.staticmock包不再可用。BeanFactoryLocator也不再可用,以及bean.factory.access包。NativeJdbcExtractor也不再可用,以及jdbc.support.nativejdbc包。web.view.tiles2orm.hibernate3orm.hibernate4包也被 Tiles 3 和 Hibernate 5 所取代。

Spring 5 中不再支持许多其他捆绑包,如 JasperReports、Portlet、Velocity、JDO、Guava、XMLBeans。如果您正在使用上述任何捆绑包,建议保持在 Spring Framework 4.3.x 上。

总结

在本章中,我们对 Spring Framework 的核心特性有了清晰的了解。我们还涵盖了不同类型的 Spring 模块。之后,我们了解了 Spring Framework 中不同类型的项目。我们还理解了 Spring IoC 容器的机制。在本章的最后,我们看了 Spring 5.0 中引入的新特性和增强功能。

在下一章中,我们将详细了解 DI 的概念。我们还将涵盖使用 DI 的不同类型的配置,包括性能评估。最后,我们将了解 DI 的陷阱。

第二章:Spring 最佳实践和 Bean 配置

在上一章中,我们了解了 Spring 框架如何实现控制反转IoC)原则。Spring IoC 是实现对象依赖关系的松耦合的机制。Spring IoC 容器是将依赖注入到对象中并使其准备好供我们使用的程序。Spring IoC 也被称为依赖注入。在 Spring 中,您的应用程序的对象由 Spring IoC 容器管理,也被称为bean。Bean 是由 Spring IoC 容器实例化、组装和管理的对象。因此,Spring 容器负责在您的应用程序中创建 bean,并通过依赖注入协调这些对象之间的关系。但是,开发人员有责任告诉 Spring 要创建哪些 bean 以及如何配置它们。在传达 bean 的装配配置时,Spring 非常灵活,提供不同的配置方式。

在本章中,我们首先开始探索不同的 bean 装配配置。这包括使用 Java、XML 和注解进行配置,以及学习 bean 装配配置的不同最佳实践。我们还将了解不同配置的性能评估,以及依赖注入的缺陷。

本章将涵盖以下主题:

  • 依赖注入配置

  • 不同配置的性能评估

  • 依赖注入的缺陷

依赖注入配置

在任何应用程序中,对象与其他对象协作执行一些有用的任务。在任何应用程序中,一个对象与另一个对象之间的关系会创建依赖关系,这种对象之间的依赖关系会在应用程序中创建紧耦合的编程。Spring 为我们提供了一种机制,将紧耦合的编程转换为松耦合的编程。这种机制称为依赖注入DI)。DI 是一种描述如何创建松耦合类的概念或设计模式,其中对象以一种方式设计,它们从其他代码片段接收对象的实例,而不是在内部构造它们。这意味着对象在运行时获得它们的依赖关系,而不是在编译时。因此,通过 DI,我们可以获得一个解耦的结构,为我们提供了简化的测试、更大的可重用性和更好的可维护性。

在接下来的章节中,我们将学习不同类型的 DI 配置,您可以根据业务需求在应用程序的任何配置中使用这些配置。

依赖注入模式的类型

在 Spring 中,进行以下类型的 DI:

  • 基于构造函数的依赖注入

  • 基于 setter 的依赖注入

  • 基于字段的依赖注入

我们将在接下来的章节中了解更多相关内容。

基于构造函数的依赖注入

基于构造函数的依赖注入是一种设计模式,用于解决依赖对象的依赖关系。在基于构造函数的依赖注入中,使用构造函数来注入依赖对象。当容器调用带有一定数量参数的构造函数时,就完成了这个过程。

让我们看一个基于构造函数的 DI 的例子。在以下代码中,我们展示了如何在BankingService类中使用构造函数来注入CustomerService对象:

@Component
public class BankingService {

  private CustomerService customerService;

  // Constructor based Dependency Injection
  @Autowired
  public BankingService(CustomerService customerService) {
    this.customerService = customerService;
  }

  public void showCustomerAccountBalance() {
    customerService.showCustomerAccountBalance();
  }

}

以下是另一个依赖类文件CustomerServiceImpl.java的内容:

public class CustomerServiceImpl implements CustomerService {

  @Override
  public void showCustomerAccountBalance() {
    System.out.println("This is call customer services");
  }
}

CustomerService.java接口的内容如下:

public interface CustomerService {  
  public void showCustomerAccountBalance(); 
}

构造函数 DI 的优势

以下是在 Spring 应用程序中使用基于构造函数的 DI 的优势:

  • 适用于必需的依赖关系。在基于构造函数的依赖注入中,您可以确保对象在构造时已经准备好供使用。

  • 代码结构非常紧凑且易于理解。

  • 当您需要一个不可变对象时,通过基于构造函数的依赖,您可以确保获得对象的不可变性。

构造函数 DI 的缺点

构造函数注入的唯一缺点是可能会导致对象之间的循环依赖。循环依赖意味着两个对象彼此依赖。为了解决这个问题,我们应该使用设置器注入而不是构造函数注入。

让我们看一种在 Spring 中不同类型的 DI,即基于设置器的注入。

设置器 DI

在基于构造函数的 DI 中,我们看到一个依赖对象通过构造函数参数注入。在基于设置器的 DI 中,依赖对象是由依赖类中的设置器方法提供的。通过在容器中调用no-args构造函数后,在 bean 上调用设置器方法来实现设置器 DI。

在下面的代码中,我们展示了如何在BankingService类中使用一个设置器方法来注入CustomerService对象:

@Component
public class BankingService {

  private CustomerService customerService;  

  // Setter-based Dependency Injection
  @Autowired
  public void setCustomerService(CustomerService customerService) {
  this.customerService = customerService;
  }

  public void showCustomerAccountBalance() {
    customerService.showCustomerAccountBalance();
  }

}

设置器 DI 的优势

以下是在您的 Spring 应用程序中设置器 DI 的优势:

  • 它比构造函数注入更可读。

  • 这对于非强制性的依赖是有用的。

  • 它解决了应用程序中的循环依赖问题。

  • 它帮助我们只在需要时注入依赖关系。

  • 可以重新注入依赖关系。这在基于构造函数的注入中是不可能的。

设置器 DI 的缺点

尽管基于设置器的 DI 优先级高于基于构造函数的 DI,但前者的缺点如下:

  • 在设置器 DI 中,不能保证依赖关系会被注入。

  • 可以使用设置器 DI 来覆盖另一个依赖关系。这可能会在 Spring 应用程序中引起安全问题。

基于字段的 DI

在前面的章节中,我们看到了如何在我们的应用程序中使用基于构造函数和基于设置器的依赖关系。在下面的示例中,我们将看到基于字段的 DI。实际上,基于字段的 DI 易于使用,并且与其他两种注入方法相比,代码更清晰;然而,它也有一些严重的折衷,通常应该避免使用。

让我们看一下基于字段的 DI 的以下示例。在下面的代码中,我们将看到如何在BankingService类中使用字段来注入CustomerService对象:

@Component
public class BankingService {

  //Field based Dependency Injection
  @Autowired
  private CustomerService customerService;

  public void showCustomerAccountBalance() {
    customerService.showCustomerAccountBalance();
  }

}

正如我们讨论过的,这种类型的 DI 有利于消除基于设置器或构造函数的依赖的混乱代码,但它也有许多缺点,比如依赖关系对外部是不可见的。在基于构造函数和基于设置器的依赖关系中,类明确地使用public接口或设置器方法来暴露这些依赖关系。在基于字段的 DI 中,类本质上是在对外部世界隐藏依赖关系。另一个困难是字段注入不能用于为 final/不可变字段分配依赖关系,因为这些字段必须在类实例化时实例化。

一般来说,Spring 不鼓励使用基于字段的依赖。

以下是我们迄今为止学到的不同类型的 DI 的图表:

构造函数与设置器注入

正如我们所看到的,Spring 支持三种 DI 方法;然而,Spring 不推荐使用基于字段的依赖。因此,基于构造函数和基于设置器的 DI 是在应用程序中注入 bean 的标准方式。构造函数或设置器方法的选择取决于您的应用程序要求。在这个表中,我们将看到构造函数和设置器注入的不同用例,以及一些最佳实践,这将帮助我们决定何时使用设置器注入而不是构造函数注入,反之亦然:

构造函数注入 设置器注入
依赖关系是强制性时的最佳选择。 依赖关系不是强制性时的合适选择。
构造函数注入使得 bean 类对象是不可变的。 设置器注入使得 bean 类对象是可变的。
构造函数注入无法覆盖 setter 注入的值。 当我们同时为同一属性使用构造函数和 setter 注入时,setter 注入会覆盖构造函数注入。
部分依赖在构造函数注入中是不可能的,因为我们必须在构造函数中传递所有参数,否则会出错。 部分依赖在 setter 注入中是可能的。假设我们有三个依赖,比如intstringlong,那么借助 setter 注入,我们可以只注入所需的依赖;其他依赖将被视为这些原始类型的默认值。
在对象之间创建循环依赖。 解决应用程序中的循环依赖问题。在循环依赖的情况下,最好使用 setter 而不是构造函数注入。

使用 Spring 配置 DI

在本节中,我们将学习不同类型的配置 DI 的过程。以下图表是配置过程在 Spring 中如何工作的高级视图:

根据前面的图表,Spring 容器负责在您的应用程序中创建 bean,并通过 DI 模式建立这些 bean 之间的关系;但是,正如我们之前讨论的,开发人员有责任通过元数据告诉 Spring 容器如何创建 bean 以及如何将它们连接在一起。

以下是配置应用程序元数据的三种技术:

  • 基于 XML 的配置:显式配置

  • 基于 Java 的配置:显式配置

  • 基于注解的配置:隐式配置

在 Spring 框架中,有前述三种配置机制可用,但您必须使用其中一种配置过程来连接您的 bean。在下一节中,我们将详细了解每种配置技术的示例,并看到在每种情况或条件下哪种技术更适合;但是,您可以使用最适合您的任何技术或方法。

现在让我们详细了解基于 XML 的配置中的 DI 模式。

基于 XML 的配置

基于 XML 的配置自 Spring 开始以来一直是主要的配置技术。在本节中,我们将看到与 DI 模式中讨论的相同示例,并看到如何通过基于 XML 的配置在BankingService类中注入CustomerService对象。

对于基于 XML 的配置,我们需要创建一个带有<beans>元素的applicationContext.xml文件。Spring 容器必须能够管理应用程序中的一个或多个 bean。使用顶级<beans>元素内部的<bean>元素来描述 bean。

以下是applicationContext.xml文件的内容:

<?xml version="1.0" encoding="UTF-8"?>
<beans 

  xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- Bean Configuration definition describe here -->
    <bean class=""/>

</beans> 

前面的 XML 文件是基于 XML 的配置元数据的基本结构,我们需要在其中定义我们的 bean 配置。正如我们之前学到的,我们的 bean 配置模式可能是基于构造函数或基于 setter 的,具体取决于应用程序的要求。现在,我们将逐个看看如何使用这两种设计模式配置 bean。

以下是基于 XML 的构造函数 DI 的示例:

<!-- CustomerServiceImpl Bean -->
<bean id="customerService"    class="com.packt.springhighperformance.ch2.bankingapp.service.Impl.CustomerServiceImpl" />

<!-- Inject customerService via constructor argument -->
<bean id="bankingService"
class="com.packt.springhighperformance.ch2.bankingapp.model.BankingService">
<constructor-arg ref="customerService" />
</bean>

在前面的例子中,我们使用构造函数 DI 模式在BankingServices类中注入了CustomerService对象。</constructor-arg>元素的ref属性用于传递CustomerServiceImpl对象的引用。

以下是基于 XML 的 setter 注入 DI 的示例:

<!-- CustomerServiceImpl Bean -->
<bean id="customerService"    class="com.packt.springhighperformance.ch2.bankingapp.service.Impl.CustomerServiceImpl" />

<!-- Inject customerService via setter method -->
<bean id="bankingService" class="com.packt.springhighperformance.ch2.bankingapp.model.BankingService"> 
<property name="customerService" ref="customerService"></property></bean>

</property>元素的ref属性用于将CustomerServiceImpl对象的引用传递给 setter 方法。

以下是MainApp.java文件的内容:

public class MainApp {

public static void main(String[] args) {
    @SuppressWarnings("resource")
    ApplicationContext context = new               
    ClassPathXmlApplicationContext("applicationContext.xml");
    BankingService bankingService = 
    context.getBean("bankingService",                            
    BankingService.class);
    bankingService.showCustomerAccountBalance(); 
  }
}

基于 Java 的配置

在上一节中,我们看到了如何使用基于 XML 的配置来配置 bean。在本节中,我们将看到基于 Java 的配置。与 XML 相同,基于 Java 的配置也是显式地注入依赖关系。以下示例定义了 Spring bean 及其依赖关系:

@Configuration
public class AppConfig { 

  @Bean
  public CustomerService showCustomerAccountBalance() {
    return new CustomerService();
  }

  @Bean
  public BankingService getBankingService() {
    return new BankingService();
  }  
}

在基于 Java 的配置中,我们必须使用@Configuration对类进行注解,并且可以使用@Bean注解来声明 bean。前面的基于 Java 的配置示例等同于基于 XML 的配置,如下所示:

<beans>
<bean id="customerService"   class="com.packt.springhighperformance.ch2.bankingapp.service.Impl.CustomerServiceImpl" /> 

<bean id="bankingService"
class="com.packt.springhighperformance.ch2.bankingapp.model.BankingService/">
</beans>

之前的AppConfig类使用了@Configuration注解,描述了它是应用程序的配置类,包含有关 bean 定义的详细信息。该方法使用@Bean注解进行注解,以描述它负责实例化、配置和初始化一个新的 bean,由 Spring IoC 容器进行管理。在 Spring 容器中,每个 bean 都有一个唯一的 ID。无论哪个方法使用了@Bean注解,那么默认情况下该方法名称将是 bean 的 ID;但是,您也可以使用@Bean注解的name属性来覆盖该默认行为,如下所示:

@Bean(name="myBean")
  public CustomerService showCustomerAccountBalance() {
    return new CustomerService();
  }

Spring 应用程序上下文将加载AppConfig文件并为应用程序创建 bean。

以下是MainApp.java文件:

public class MainApp {

  public static void main(String[] args) {
    AnnotationConfigApplicationContext context = new                                                 
    AnnotationConfigApplicationContext(AppConfig.class);
    BankingService bankingService = 
    context.getBean(BankingService.class);
    bankingService.showCustomerAccountBalance();
    context.close();     
  }
}

基于注解的配置

在上一节中,我们看到了两种 bean 配置技术,即基于 Java 和基于 XML 的。这两种技术都是显式地注入依赖关系。在基于 Java 的配置中,我们在AppConfig Java 文件中使用@Bean注解的方法,而在基于 XML 的配置中,我们在 XML 配置文件中使用<bean>元素标签。基于注解的配置是另一种创建 bean 的方式,我们可以通过在相关类、方法或字段声明上使用注解,将 bean 配置移到组件类本身。在这里,我们将看看如何通过注解配置 bean,以及 Spring Framework 中提供的不同注解。

Spring 中默认情况下关闭了基于注解的配置,因此首先,您必须通过在 Spring XML 文件中输入<context:annotation-config/>元素来打开它。添加后,您就可以在代码中使用注解了。

applicationContext.xml中需要进行的更改(因为我们在之前的部分中使用了它)如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans 

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

<!-- Enable Annotation based configuration -->
<context:annotation-config />
<context:component-scan base-package="com.packt.springhighperformance.ch2.bankingapp.model"/><context:component-scan base- package="com.packt.springhighperformance.ch2.bankingapp.service"/>

<!-- Bean Configuration definition describe here -->
<bean class=""/>

</beans>

基于 XML 的配置将覆盖注解,因为基于 XML 的配置将在注解之后进行注入。

之前的基于 XML 的配置显示,一旦配置了<context:annotation-config/>元素,就表示开始对代码进行注解。Spring 应该自动扫描在<context:component-scan base-package=".." />中定义的包,并根据模式识别 bean 并进行连线。让我们了解一些重要的注解以及它们的工作原理。

@Autowired 注解

@Autowired注解隐式地注入对象依赖。我们可以在基于构造函数、setter 和字段的依赖模式上使用@Autowired注解。@Autowired注解表示应该为此 bean 执行自动装配。

让我们看一个在基于构造函数的依赖注入上使用@Autowired注解的例子:

public class BankingService {  

  private CustomerService customerService;

  @Autowired
  public BankingService(CustomerService customerService) {
    this.customerService = customerService;
  }
  ......
}

在前面的例子中,我们有一个BankingService,它依赖于CustomerService。它的构造函数使用@Autowired进行注解,表示 Spring 使用带注解的构造函数实例化BankingService bean,并将CustomerService bean 作为BankingService bean 的依赖项。

自 Spring 4.3 以来,对于只有一个构造函数的类,@Autowired注解变得可选。在前面的例子中,如果您跳过了@Autowired注解,Spring 仍然会注入CustomerService类的实例。

让我们看一个在基于 setter 的依赖注入上使用@Autowired注解的例子:

public class BankingService {

  private CustomerService customerService; 

  @Autowired
  public void setCustomerService(CustomerService customerService) {
    this.customerService = customerService;
  }
  ......
}

在前面的例子中,我们看到 setter 方法setCustomerService@Autowired注解标记。在这里,注解通过类型解析依赖关系。@Autowire注解可以用于任何传统的 setter 方法。

让我们看一个在基于字段的依赖上使用@Autowired注解的例子:

public class BankingService {

  @Autowired
  private CustomerService customerService; 

}

根据前面的例子,我们可以看到@Autowire注解可以添加在公共和私有属性上。Spring 在属性上添加时使用反射 API 来注入依赖项,这就是私有属性也可以被注解的原因。

@Autowired with required = false

默认情况下,@Autowired注解意味着依赖是必需的。这意味着在未解析依赖项时将抛出异常。您可以使用@Autowired(required=false)选项覆盖默认行为。让我们看下面的代码:

public class BankingService {

  private CustomerService customerService; 

  @Autowired (required=false)
  public void setCustomerService(CustomerService customerService) {
    this.customerService = customerService;
  }
  ......
}

在前面的代码中,如果我们将required值设置为false,那么在 bean 连线时,如果依赖项未解析,Spring 将保留 bean 未连接。根据 Spring 的最佳实践,我们应该避免将required设置为false,除非绝对需要。

@Primary 注解

在 Spring 框架中,默认情况下,DI 是按类型完成的,这意味着当存在多个具有相同类型的依赖项时,将抛出NoUniqueBeanDefinitionException异常。这表明 Spring 容器无法选择要注入的 bean,因为有多个合格的候选项。在这种情况下,我们可以使用@Primary注解并控制选择过程。让我们看下面的代码:

public interface CustomerService {  
  public void customerService(); 
}

@Component
public class AccountService implements CustomerService {
      ....
}
@Component
@Primary
public class BankingService implements CustomerService { 
     ....
}

在前面的例子中,有两个客户服务可用:BankingServiceAccountService。由于@Primary注解,组件只能使用BankingService来连线CustomerService的依赖项。

@Qualifier 注解

使用@Primary处理多个自动装配候选项在只能确定一个主要候选项的情况下更有效。@Qualifier注解在选择过程中给予更多控制。它允许您给出与特定 bean 类型相关联的引用。该引用可用于限定需要自动装配的依赖项。让我们看下面的代码:

@Component
public class AccountService implements CustomerService {

}
@Component
@Qualifier("BankingService")
public class BankingService implements CustomerService { 

}

@Component
public class SomeService {

  private CustomerService customerService;

  @Autowired
  @Qualifier("bankingservice")
  public BankingService(CustomerService customerService) {
    this.customerService = customerService;
  }
  .....
}

在前面的例子中,有两个客户服务可用:BankingServiceAccountService;但是,由于在SomeService类中使用了@Qualifier("bankingservice")BankingService将被选中进行自动连线。

使用原型注解自动检测 bean

在前一节中,我们了解了@Autowired注解只处理连线。您仍然必须定义 bean 本身,以便容器知道它们并为您注入它们。Spring 框架为我们提供了一些特殊的注解。这些注解用于在应用程序上下文中自动创建 Spring bean。因此,无需使用基于 XML 或基于 Java 的配置显式配置 bean。

以下是 Spring 中的原型注解:

  • @Component

  • @Service

  • @Repository

  • @Controller

让我们看一下以下CustomerService实现类。它的实现被注解为@Component。请参考以下代码:

@Component
public class CustomerServiceImpl implements CustomerService {

  @Override
  public void customerService() {
    System.out.println("This is call customer services");

  }

}

在前面的代码中,CustomerServiceImpl类被@Component注解标记。这意味着被标记为@Component注解的类被视为 bean,并且 Spring 的组件扫描机制扫描该类,创建该类的 bean,并将其拉入应用程序上下文。因此,无需显式配置该类作为 bean,因为 bean 是使用 XML 或 Java 自动创建的。Spring 自动创建CustomerServiceImpl类的 bean,因为它被@Component注解标记。

在 Spring 中,@Service@Repository@Controller@Component注解的元注解。从技术上讲,所有注解都是相同的,并提供相同的结果,例如在 Spring 上下文中创建一个 bean;但是我们应该在应用程序的不同层次使用更具体的注解,因为它更好地指定了意图,并且将来可能会依赖于其他行为。

以下图表描述了具有适当层的原型注解:

根据前面的例子,@Component足以创建CustomerService的 bean。但是CustomerService是一个服务层类,因此根据 bean 配置最佳实践,我们应该使用@Services而不是通用的@Component注解。让我们看一下相同类的以下代码,该类使用了@Service注解:

@Service
public class CustomerServiceImpl implements CustomerService {

  @Override
  public void customerService() {
    System.out.println("This is call customer services");
  }

}

让我们看一个@Repository注解的另一个例子:

@Repository
public class JdbcCustomerRepository implements CustomerRepository {

}

在前面的例子中,该类被注解为@Repository,因为CustomerRepository接口在应用程序的数据访问对象DAO)层中起作用。根据 bean 配置最佳实践,我们使用了@Repository注解而不是@Component注解。

在现实场景中,您可能会很少遇到需要使用@Component注解的情况。大多数情况下,您将使用@Controller@Service@Repository注解。当您的类不属于服务、控制器、DAO 三类时,应该使用@Component

@ComponentScan 注解

Spring 需要知道哪些包包含 Spring bean,否则,您将需要逐个注册每个 bean。这就是@ComponentScan的用途。在 Spring 中,默认情况下不启用组件扫描。我们需要使用@ComponentScan注解来启用它。此注解与@Configuration注解一起使用,以便 Spring 知道要扫描的包,并从中创建 bean。让我们看一个简单的@ComponentScan的例子:

@Configuration
@ComponentScan(basePackages="com.packt.springhighperformance.ch2.bankingapp.model")
public class AppConfig {

}

@ComponentScan注解中,如果未定义basePackages属性,则扫描将从声明此注解的类的包中进行。在前面的例子中,Spring 将扫描com.packt.springhighperformance.ch2.bankingapp.model的所有类,以及该包的子包。basePackages属性可以接受一个字符串数组,这意味着我们可以定义多个基本包来扫描应用程序中的组件类。让我们看一个如何在basePackage属性中声明多个包的例子:

@Configuration
@ComponentScan(basePackages={"com.packt.springhighperformance.ch2.bankingapp.model","com.packt.springhighperformance.ch2.bankingapp.service"})
public class AppConfig {

}

@Lazy 注解

默认情况下,所有自动装配的依赖项都会在启动时创建和初始化,这意味着 Spring IoC 容器会在应用程序启动时创建所有 bean;但是,我们可以使用@Lazy注解来控制这种预初始化的 bean。

@Lazy注解可以用于任何直接或间接使用@Component注解的类,或者用于使用@Bean注解的方法。当我们使用@Lazy注解时,这意味着只有在首次请求时才会创建和初始化 bean。

我们知道注解需要的代码较少,因为我们不需要显式编写代码来注入依赖项。它还有助于减少开发时间。尽管注解提供了许多优点,但它也有缺点。

注解的缺点如下:

  • 比显式连线文档少

  • 如果程序中有很多依赖项,那么使用 bean 的autowire属性来查找它是很困难的。

  • 注解使调试过程变得困难

  • 在存在歧义的情况下可能会产生意外结果

  • 注解可以被显式配置(如 Java 或 XML)覆盖

Spring bean 作用域

在前一节中,我们学习了各种 DI 模式,以及如何在 Spring 容器中创建 bean。我们还学习了各种 DI 配置,如 XML、Java 和注解。在本节中,我们将更详细地了解 Spring 容器中可用的 bean 生命周期和范围。Spring 容器允许我们在配置级别控制 bean。这是一种非常灵活的方式,可以在配置级别定义对象范围,而不是在 Java 类级别。在 Spring 中,通过定义scope属性来控制 bean 的行为,该属性定义了要创建和返回的对象类型。当您描述<bean>时,可以为该 bean 定义scope。bean scope描述了 bean 在使用的上下文中的生命周期和可见性。在本节中,我们将看到 Spring Framework 中不同类型的 bean scope

以下是在基于 XML 的配置中定义 bean scope的示例:

<?xml version="1.0" encoding="UTF-8"?>
<beans 

xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- Here scope is not defined, it assume default value 'singleton'.
    It creates only one instance per spring IOC. -->
<bean id="customerService" class="com.packt.springhighperformance.ch2.bankingapp.service.Impl.CustomerServiceImpl" />

<!-- Here scope is prototype, it creates and returns bankingService object for  every call-->
<bean id="bankingService"   class="com.packt.springhighperformance.ch2.bankingapp.model.BankingService" scope="prototype">

<bean id="accountService" class="com.packt.springhighperformance.ch2.bankingapp.model.AccountService" scope="singleton">

</beans>

以下是使用@Scope注解定义 bean scope的示例:

@Configuration
public class AppConfig { 

  @Bean
  @Scope("singleton")
  public CustomerService showCustomerAccountBalance() {
    return new CustomerServiceImpl();

  }
}

我们也可以以以下方式使用常量而不是字符串值:

@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)

以下是 Spring Framework 中可用的 bean 范围:

  • singleton bean scope:正如我们在之前的 XML 配置的 bean 示例中看到的,如果在配置中未定义scope,那么 Spring 容器将scope视为singleton。Spring IoC 容器仅创建对象的一个单一实例,即使有多个对 bean 的引用。Spring 将所有singleton bean 实例存储在缓存中,对该命名 bean 的所有后续请求都返回缓存对象。需要理解的是,Spring bean singleton scope与我们在 Java 中使用的典型singleton设计模式有些不同。在 Spring singleton scope中,每个 Spring 容器创建一个 bean 对象,这意味着如果单个 JVM 中有多个 Spring 容器,则将创建该 bean 的多个实例。

  • prototype bean scope:当scope设置为prototype时,Spring IoC 容器在每次请求 bean 时都会创建对象的新 bean 实例。通常使用原型作用域的 bean 用于有状态的 bean。

通常,对于所有有状态的 bean 使用prototype scope,对于无状态的 bean 使用singleton scope

  • request bean scoperequest bean scope仅在 Web 应用程序上下文中可用。request scope为每个 HTTP 请求创建一个 bean 实例。一旦请求处理完成,bean 就会被丢弃。

  • session bean scopesession bean scope仅在 Web 应用程序上下文中可用。session scope为每个 HTTP 会话创建一个 bean 实例。

  • application bean scopeapplication bean scope仅在 Web 应用程序上下文中可用。application scope为每个 Web 应用程序创建一个 bean 实例。

使用不同配置进行性能评估

在本节中,我们将学习不同类型的 bean 配置如何影响应用程序性能,还将看到 bean 配置的最佳实践。

让我们看看@ComponentScan注解配置如何影响 Spring 应用程序的启动时间:

@ComponentScan (( {{ "org", "com" }} ))

根据前面的配置,Spring 将扫描comorg的所有包,因此应用程序的启动时间将增加。因此,我们应该只扫描那些具有注释类的包,因为未注释的类将花费时间进行扫描。我们应该只使用一个@ComponentScan,并列出所有包,如下所示:

@ComponentScan(basePackages={"com.packt.springhighperformance.ch2.bankingapp.model","com.packt.springhighperformance.ch2.bankingapp.service"})

前面的配置被认为是定义@ComponentScan注解的最佳实践。我们应该指定哪些包作为basePackage属性具有注释类。这将减少应用程序的启动时间。

延迟加载与预加载

延迟加载确保在请求时动态加载 bean,而预加载确保在使用之前加载 bean。Spring IoC 容器默认使用预加载。因此,在启动时加载所有类,即使它们没有被使用,也不是一个明智的决定,因为一些 Java 实例会消耗大量资源。我们应该根据应用程序的需求使用所需的方法。

如果我们需要尽可能快地加载我们的应用程序,那么选择延迟加载。如果我们需要尽可能快地运行我们的应用程序并更快地响应客户端请求,那么选择预加载。

单例与原型 bean

在 Spring 中,默认情况下,所有定义的 bean 都是singleton;但是,我们可以更改默认行为并使我们的 bean 成为prototype。当 bean 的scope设置为prototype时,Spring IoC 容器在每次请求 bean 时创建一个新的 bean 实例。原型 bean 在创建时会对性能造成影响,因此当一个prototype bean 使用资源(如网络和数据库连接)时,应完全避免;或者谨慎设计操作。

Spring bean 配置最佳实践

在本节中,我们将看到 Spring 配置 bean 的一些最佳实践:

  • 使用 ID 作为 bean 标识符:
<?xml version="1.0" encoding="UTF-8"?>
<beans 

xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- Bean Configuration definition describe here -->
    <bean id="xxx" name="xxx" class=""/>

</beans>

在前面的例子中,我们使用idname来标识 bean。我们应该使用id来选择 bean 而不是name。通常,它既不增加可读性也不提高性能,但这只是一个行业标准实践,我们需要遵循。

  • 在构造函数参数匹配时,优先使用type而不是index。带有index属性的构造函数参数如下所示:
<constructor-arg index="0" value="abc"/>
<constructor-arg index="1" value="100"/>
  • 构造函数参数带有type属性,如下所示:
<constructor-arg type="java.lang.String"
value="abc"/>
<constructor-arg type="int" value="100"/>

根据前面的例子,我们可以使用indextype作为构造函数参数。在构造函数参数中最好使用type属性而不是index,因为它更易读且更少出错。但有时,基于类型的参数可能会在构造函数有多个相同类型的参数时创建歧义问题。在这种情况下,我们需要使用index或基于名称的参数。

  • 在开发阶段使用依赖检查:在 bean 定义中,我们应该使用dependency-check属性。它确保容器执行显式的依赖验证。当一个 bean 的所有或部分属性必须显式设置或通过自动装配时,它是有用的。

  • 不要在 Spring 模式引用中指定版本号:在 Spring 配置文件中,我们指定不同 Spring 模块的模式引用。在模式引用中,我们提到 XML 命名空间及其版本号。在配置文件中指定版本号不是强制性的,因此您可以跳过它。事实上,您应该始终跳过它。将其视为一种最佳实践。Spring 会自动从项目依赖项(jars)中选择最高版本。典型的 Spring 配置文件如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans 

xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <!-- Bean Configuration definition describe here -->
    <bean class=""/>

</beans>

根据最佳实践,可以这样编写:

<?xml version="1.0" encoding="UTF-8"?>
<beans 

xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- Bean Configuration definition describe here -->
    <bean class=""/>

</beans>

为每个配置文件添加一个头部注释;最好添加一个描述配置文件中定义的 bean 的配置文件头部。description标签的代码如下:

<beans>
<description>
This file defines customer service
related beans and it depends on
accountServices.xml, which provides
service bean templates...
</description>
...
</beans>

description标签的优点是一些工具可以从这个标签中获取描述,以帮助您在其他地方使用。

DI 陷阱

众所周知,在 Spring 应用程序中有三种 DI 模式:构造函数、setter 和基于字段。每种类型都有不同的优缺点。只有基于字段的 DI 是一种错误的方法,甚至 Spring 也不推荐使用。

以下是基于字段的注入的示例:

@Autowired
private ABean aBean;

根据 Spring bean 最佳实践,我们不应该在 Spring 应用程序中使用基于字段的依赖。主要原因是没有 Spring 上下文无法进行测试。由于我们无法从外部提供依赖,因此无法独立实例化对象。在我看来,这是基于字段的注入唯一的问题。

正如我们在前面的部分中学到的,基于构造函数的依赖更适合于必填字段,并且我们可以确保对象的不可变性;然而,基于构造函数的依赖的主要缺点是它在应用程序中创建循环依赖,并且根据 Spring 文档,通常建议不要依赖 bean 之间的循环依赖。因此,现在我们有类似的问题,为什么不依赖循环依赖?如果我们的应用程序中有循环依赖会发生什么?。因此,对这些问题的答案是它可能会产生两个重大且不幸的潜在问题。让我们讨论一下。

第一个潜在问题

当您调用ListableBeanFactory.getBeansOfType()方法时,您无法确定将返回哪些 bean。让我们看一下DefaultListableBeanFactory.java类中getBeansOfType()方法的代码:

@Override
@SuppressWarnings("unchecked")
public <T> Map<String, T> getBeansOfType(@Nullable Class<T> type, boolean includeNonSingletons, boolean allowEagerInit)
      throws BeansException {

      ......

      if (exBeanName != null && isCurrentlyInCreation(exBeanName)) {
        if (this.logger.isDebugEnabled()) {
          this.logger.debug("Ignoring match to currently created bean 
          '" + 
          exBeanName + "': " +
          ex.getMessage());
        }
        onSuppressedException(ex);
        // Ignore: indicates a circular reference when auto wiring 
        constructors.
        // We want to find matches other than the currently created 
        bean itself.
        continue;
      }

      ......

}

在上面的代码中,您可以看到getBeansOfType()方法在创建中默默地跳过 bean,并且只返回那些已经存在的。因此,当 bean 之间存在循环依赖时,在容器启动期间不建议使用getBeansOfType()方法。这是因为,根据上面的代码,如果您没有使用DEBUGTRACE日志级别,那么您的日志中将没有任何信息表明 Spring 跳过了正在创建的特定 bean。

让我们看看前面的潜在问题以及以下示例。根据以下图表,我们有三个 bean,AccountCustomerBank,它们之间存在循环依赖:

根据前面的图表,以下是AccountCustomerBank类:

@Component
public class Account {

  private static final Logger LOGGER = Logger.getLogger(Account.class);

  static {
    LOGGER.info("Account | Class loaded");
  }

  @Autowired
  public Account(ListableBeanFactory beanFactory) {
    LOGGER.info("Account | Constructor");
    LOGGER.info("Constructor (Customer?): {}" + 
    beanFactory.getBeansOfType(Customer.class).keySet());
    LOGGER.info("Constructor (Bank?): {}" + 
    beanFactory.getBeansOfType(Bank.class).keySet());
  }

}

@Component
public class Customer {

  private static final Logger LOGGER = Logger.getLogger(Customer.class);

  static {
    LOGGER.info("Customer | Class loaded");
  }

  @Autowired
  public Customer(ListableBeanFactory beanFactory) {
    LOGGER.info("Customer | Constructor");
    LOGGER.info("Account (Account?): {}" + 
    beanFactory.getBeansOfType(Account.class).keySet());
    LOGGER.info("Constructor (Bank?): {}" + 
    beanFactory.getBeansOfType(Bank.class).keySet());
  }

}

@Component
public class Bank {

  private static final Logger LOGGER = Logger.getLogger(Bank.class);

  static {
    LOGGER.info("Bank | Class loaded");
  }

  public Bank() {
    LOGGER.info("Bank | Constructor");
  }

}

以下是Main类:

public class MainApp {

  public static void main(String[] args) {
    AnnotationConfigApplicationContext context = new 
    AnnotationConfigApplicationContext(AppConfig.class);
    Account account = context.getBean(Account.class);
    context.close();
  }
}

以下是日志,我们可以展示 Spring 如何内部加载 bean 并解析类:

Account | Class loaded
Account | Constructor
Customer | Class loaded
Customer | Constructor
Account (Account?): {}[]
Bank | Class loaded
Bank | Constructor
Constructor (Bank?): {}[bank]
Constructor (Customer?): {}[customer]
Constructor (Bank?): {}[bank]

Spring Framework 首先加载Account并尝试实例化一个 bean;然而,在运行getBeansOfType(Customer.class)时,它发现了Customer,因此继续加载和实例化那个。在Customer内部,我们可以立即发现问题:当Customer要求beanFactory.getBeansOfType(Account.class)时,它得不到结果([])。Spring 会默默地忽略Account,因为它当前正在创建。您可以在这里看到,在加载Bank之后,一切都如预期那样。

现在我们可以理解,在有循环依赖时,我们无法预测getBeansOfType()方法的输出。然而,我们可以通过正确使用 DI 来避免它。在循环依赖中,getBeansOfType()根据因素给出不同的结果,我们对此没有任何控制。

第二个潜在问题(带 AOP)

我们将在下一章中详细学习 AOP。现在,我们不会详细介绍这个潜在问题。我只是想让你明白,如果你在一个 bean 上有Aspect,那么请确保 bean 之间没有循环依赖;否则,Spring 将创建该 bean 的两个实例,一个没有Aspect,另一个有适当的方面,而不通知您。

总结

在本章中,我们学习了 DI,这是 Spring Framework 的关键特性。DI 帮助我们使我们的代码松散耦合和可测试。我们学习了各种 DI 模式,包括基于构造函数、setter 和字段的模式。根据我们的需求,我们可以在我们的应用程序中使用任何 DI 模式,因为每种类型都有其自己的优缺点。

我们还学习了如何显式和隐式地配置 DI。我们可以使用基于 XML 和基于 Java 的配置显式地注入依赖关系。注解用于隐式地注入依赖关系。Spring 为我们提供了一种特殊类型的注解,称为原型注解。Spring 将自动注册用原型注解注释的类。这使得该类可以在其他类中进行 DI,并且对于构建我们的应用程序至关重要。

在下一章中,我们将看一下 Spring AOP 模块。AOP 是一个强大的编程模型,可以帮助我们实现可重用的代码。

第三章:调整面向切面编程

在上一章中,我们深入研究了 Spring 的一个关键特性:依赖注入(IoC 容器)。DI 是一种企业设计模式,使对象与其所需的依赖关系解耦。我们了解了 Spring 的 bean 装配配置和实现最佳实践以实现最佳结果。

在继续了解 Spring 的核心特性的同时,在本章中,我们将讨论面向切面编程AOP)。我们已经了解到 DI 促进了编程到接口和应用对象的解耦,而 AOP 有助于实现业务逻辑和横切关注点的解耦。横切关注点是应用程序部分或整个应用程序适用的关注点,例如安全、日志记录和缓存,在几乎每个模块中都需要。AOP 和 AspectJ 有助于实现这些横切关注点。在本章中,我们将讨论以下主题:

  • AOP 概念

  • AOP 代理

  • Spring AOP 方法进行性能分析

  • AOP 与 AspectJ 比较

  • AOP 最佳编程实践

AOP 概念

在本节中,我们将看看如果只使用面向对象编程OOP)范例,我们将面临哪些问题。然后我们将了解 AOP 如何解决这些问题。我们将深入了解 AOP 的概念和实现 AOP 概念的方法。

OOP 的局限性

借助 OOP 的基本原理和设计模式,应用程序开发被划分为功能组。OOP 协议使许多事情变得简单和有用,例如引入接口,我们可以实现松耦合设计,封装,我们可以隐藏对象数据,继承-通过类扩展功能,我们可以重用工作。

随着系统的增长,OOP 的这些优势也增加了复杂性。随着复杂性的增加,维护成本和失败的机会也增加。为了解决这个问题,将功能模块化为更简单和更易管理的模块有助于减少复杂性。

为了模块化系统,我们开始遵循将应用程序划分为不同逻辑层的做法,例如表示层、服务层和数据层。然而,即使将功能划分为不同层,仍然有一些功能在所有层中都是必需的,例如安全、日志记录、缓存和性能监控。这些功能被称为横切关注点

如果我们使用继承来实现这些横切关注点,将违反 SOLID 原则的单一责任,并增加对象层次结构。如果我们使用组合来实现它们,将会更加复杂。因此,使用 OOP 实现横切关注点会导致两个问题:

  • 代码交织

  • 代码分散

让我们更深入地讨论这些问题。

代码交织

代码交织意味着混合横切关注点和业务逻辑,从而导致紧耦合。让我们看下面的图表来理解代码交织:

代码交织

前面的图表说明了我们在服务实现中将事务和安全代码与业务逻辑混合在一起。通过这样的实现,代码的可重用性降低,维护性下降,并且违反了单一责任原则。

代码分散

代码分散意味着横切关注点在应用程序的所有模块中都是重复的。让我们看下面的例子来理解代码分散:

public class TransferServiceImpl implements TransferService {
  public void transfer(Account source, Account dest, Double amount) {
    //permission check
    if (!hasPermission(user) {
      throw new AuthorizationException();
    }
  }
}

public class AccountServiceImpl implements AccountService {
  public void withdraw(Account userAccount, Double amount) {
    //Permission check
    if (!hasPermission(user) {
      throw new AuthorizationException();
    }
}

正如我们在前面的代码示例中看到的,权限检查(安全性)是我们的横切关注点,在所有服务中都是重复的。

这些代码交织和代码分散的问题通过 AOP 得到解决,但是如何呢?我们很快就会看到。

AOP-问题解决者

我们已经在前面的部分中看到,使用 OOP 会导致代码交织和分散。使用 AOP,我们可以实现以下目标/好处:

  • 模块化横切关注

  • 模块解耦

  • 消除模块依赖的横切关注

Spring AOP 允许我们将横切关注逻辑与业务逻辑分开,这样我们就可以专注于应用的主要逻辑。为了帮助我们进行这种分离,Spring 提供了Aspects,这是一个普通的类,我们可以在其中实现我们的横切关注逻辑。Spring 提供了将这些Aspects注入到我们应用的正确位置的方法,而不会将它们与业务逻辑混合在一起。我们将在接下来的部分中更多地了解Aspects,如何实现它以及如何应用它。

这个图表说明了 Spring AOP:

AOP 如何解决代码交织

Spring AOP 术语和概念

AOP,就像每种技术一样,有自己的术语。它有自己的词汇。Spring 在其 Spring AOP 模块中使用 AOP 范式。但是,Spring AOP 有其自己的术语,这些术语是特定于 Spring 的。为了理解 Spring AOP 术语,让我们看一下以下图表:

Spring AOP 术语和概念

让我们了解前面图表中提到的 Spring AOP 的每个概念:

  • 连接点:程序执行中定义的点。这个执行可以是方法调用、异常处理、类初始化或对象实例化。Spring AOP 仅支持方法调用。如果我们想要除了方法调用之外的连接点,我们可以同时使用 Spring 和 AspectJ。我们将在本章后面介绍 AspectJ。

  • 建议:在连接点上需要做什么的定义。不同类型的建议包括@Before@After@Around@AfterThrowing@AfterReturning。我们将在建议类型部分看到它们的实际应用。

  • 切入点:用于定义必须执行的建议的连接点集合。建议不一定适用于所有连接点,因此切入点可以对我们应用中要执行的建议进行精细控制。切入点使用表达式定义,Spring 使用 AspectJ 切入点表达式语言。我们很快就会看到如何做到这一点。

  • 切面:建议和切入点的组合,定义了应用中的逻辑以及应该在哪里执行。切面是使用带有@Aspect注解的常规类来实现的。这个注解来自 Spring AspectJ 支持。

这太多理论了,不是吗?现在,让我们深入了解如何在实际编程中应用这些 Spring AOP 概念。您可能已经在项目中实现了这些 AOP 概念;但是,您知道为什么需要它吗?不知道,所以现在您知道为什么我们需要 Spring AOP 了。

自从 Spring 2.0 以来,AOP 的实现变得更简单,使用了 AspectJ 切入点语言,可以在基于模式的方法(XML)或注解中定义。我们将在本章的后续部分讨论 Spring 2.0 的 AspectJ 支持和注解。

定义切入点

正如我们之前学到的,切入点定义了建议应该应用的位置。Spring AOP 使用 AspectJ 的表达式语言来定义建议应该应用的位置。以下是 Spring AOP 支持的一组切入点设计器:

设计器 描述
execution 它限制匹配只在方法执行时的连接点中进行。
within 它限制匹配只在特定类型的连接点中进行。例如:within(com.packt.springhighperformance.ch3.TransferService)
args 它限制匹配只在参数为给定类型的连接点中进行。例如:args(account,..)
this 它将匹配限制在 bean 引用或 Spring 代理对象是给定类型的实例的连接点。例如:this(com.packt.springhighperformance.ch3.TransferService)
target 它将匹配限制在目标对象是给定类型实例的连接点。例如:target(com.packt.springhighperformance.ch3.TransferService)
@within 它将匹配限制在声明类型具有给定类型注解的连接点。例如:@within(org.springframework.transaction.annotation.Transactional)
@target 它将匹配限制在目标对象具有给定类型注解的连接点。例如:@target(org.springframework.transaction.annotation.Transactional)
@args 它将匹配限制在传递的实际参数类型具有给定类型注解的连接点。例如:@args(com.packt.springhighperformance.ch3.Lockable)
@annotation 它将匹配限制在执行方法具有给定注解的连接点。例如:@annotation(org.springframework.transaction.annotation.Transactional)

让我们看看如何使用execution指示符编写切入点表达式:

  • 使用execution(<method-pattern>):匹配模式的方法将被 advised。以下是方法模式:
[Modifiers] ReturnType [ClassType]
MethodName ([Arguments]) [throws ExceptionType]
  • 要通过连接其他切入点来创建复合切入点,我们可以使用&&||!运算符(分别表示 AND、OR 和 NOT)。

在前面的方法模式中,方括号[ ]中定义的内容是可选的。没有[ ]的值是必须定义的。

以下图表将说明使用execution指示符的切入点表达式,以便在执行findAccountById()方法时应用 advice:

执行连接点模式

advice 的类型

在前面的部分,我们学习了 AOP 的不同术语以及如何定义切入点表达式。在本节中,我们将学习 Spring AOP 中不同类型的 advice:

  • @Before:这个 advice 在连接点之前执行,并且在aspect中使用@Before注解进行定义。声明如下代码所示:
@Pointcut("execution(* com.packt.springhighperformance.ch03.bankingapp.service.TransferService.transfer(..))")
public void transfer() {}

@Before("transfer()")
public void beforeTransfer(JoinPoint joinPoint){
  LOGGGER.info("validate account balance before transferring amount");
}

如果@Before方法抛出异常,transfer目标方法将不会被调用。这是@Before advice 的有效用法。

  • @After:这个 advice 在连接点(方法)退出/返回时执行,无论是正常返回还是有异常。要声明这个 advice,使用@After注解。声明如下代码所示:
@Pointcut("execution(* com.packt.springhighperformance.ch03.bankingapp.service.TransferService.transfer(..))")
public void transfer() {}

@After("transfer()")
public void afterTransfer(JoinPoint joinPoint){
  LOGGGER.info("Successfully transferred from source account to dest     
  account");
}
  • @AfterReturning:正如我们在@After advice 中所知,无论连接点正常退出还是有异常,advice 都会执行。现在,如果我们只想在匹配的方法正常返回后运行 advice,怎么办?那么我们需要@AfterReturning。有时我们需要根据方法返回的值执行一些操作。在这些情况下,我们可以使用@AfterReturning注解。声明如下代码所示:
@Pointcut("execution(* com.packt.springhighperformance.ch03.bankingapp.service.TransferService.transfer(..))")
public void transfer() {}

@AfterReturning(pointcut="transfer() and args(source, dest, amount)", returning="isTransferSuccessful" )
public void afterTransferReturns(JoinPoint joinPoint, Account source, Account dest, Double amount, boolean isTransferSuccessful){
  if(isTransferSuccessful){
    LOGGGER.info("Amount transferred successfully ");
    //find remaining balance of source account
  }
}
  • @AfterThrowing:当表达式中的匹配方法抛出异常时,将调用这个 advice。当我们想要在抛出特定类型的异常时采取某些操作,或者我们想要跟踪方法执行以纠正错误时,这是很有用的。它使用@AfterThrowing注解声明,如下代码所示:
@Pointcut("execution(* com.packt.springhighperformance.ch03.bankingapp.service.TransferService.transfer(..))")
public void transfer() {}

@AfterThrowing(pointcut = "transfer()", throwing = "minimumAmountException")
public void exceptionFromTransfer(JoinPoint joinPoint, MinimumAmountException minimumAmountException) {
  LOGGGER.info("Exception thrown from transfer method: " +         
  minimumAmountException.getMessage());
}

类似于@AfterThrowing returning属性,@AfterThrowing advice 中的throwing属性必须与 advice 方法中的参数名称匹配。throwing属性将匹配那些抛出指定类型异常的方法执行。

  • @Around应用于匹配方法周围的最后一个建议。这意味着它是我们之前看到的 @Before@After 建议的组合。但是,@Around 建议比 @Before@After 更强大。它更强大,因为它可以决定是否继续到连接点方法或返回自己的值或抛出异常。@Around 建议可以与 @Around 注解一起使用。@Around 建议中建议方法的第一个参数应该是 ProceedingJoinPoint。以下是如何使用 @Around 建议的代码示例:
@Pointcut("execution(* com.packt.springhighperformance.ch03.bankingapp.service.TransferService.transfer(..))")
public void transfer() {}

@Around("transfer()")
public boolean aroundTransfer(ProceedingJoinPoint proceedingJoinPoint){
  LOGGER.info("Inside Around advice, before calling transfer method ");
  boolean isTransferSuccessful = false;
  try {
    isTransferSuccessful = (Boolean)proceedingJoinPoint.proceed();
  } catch (Throwable e) {
    LOGGER.error(e.getMessage(), e);
  }
  LOGGER.info("Inside Around advice, after returning from transfer 
  method");
  return isTransferSuccessful;
}

我们可以在 @Around 建议的主体内部一次、多次或根本不调用 proceed

Aspect 实例化模型

默认情况下,声明的 aspectsingleton,因此每个类加载器(而不是每个 JVM)只会有一个 aspect 实例。我们的 aspect 实例只有在类加载器被垃圾回收时才会被销毁。

如果我们需要让我们的 aspect 具有私有属性来保存与类实例相关的数据,那么 aspect 需要是有状态的。为此,Spring 与其 AspectJ 支持提供了使用 perthispertarget 实例化模型的方法。AspectJ 是一个独立的库,除了 perthispertarget 之外,还有其他实例化模型,如 percflowpercflowbelowpertypewithin,这些在 Spring 的 AspectJ 支持中不受支持。

要使用 perthis 创建一个有状态的 aspect,我们需要在我们的 @Aspect 声明中声明 perthis 如下:

@Aspect("perthis(com.packt.springhighperformance.ch03.bankingapp.service.TransferService.transfer())")
public class TransferAspect {
//Add your per instance attributes holding private data
//Define your advice methods
}

一旦我们用 perthis 子句声明了我们的 @Aspect,将为每个执行 transfer 方法的唯一 TransferService 对象创建一个 aspect 实例(通过切入点表达式匹配的 this 绑定到的每个唯一对象)。当 TransferService 对象超出范围时,aspect 实例也会超出范围。

pertargetperthis 的工作方式相同;但是,在 pertarget 中,它会在切入点表达式匹配的连接点上为每个唯一的目标对象创建一个 aspect 实例。

现在你可能想知道 Spring 是如何应用建议而不是从业务逻辑类到横切关注类(Aspects)进行调用的。答案是,Spring 使用代理模式来实现这一点。它通过创建代理对象将你的 Aspects 编织到目标对象中。让我们在下一节详细看一下 Spring AOP 代理。

AOP 代理

正是代理模式使得 Spring AOP 能够将横切关注从核心应用程序的业务逻辑或功能中解耦出来。代理模式是一种结构设计模式,包含在《四人组》(GoF)的一本书中。在实践中,代理模式通过创建不同的对象包装原始对象,而不改变原始对象的行为,以允许拦截其方法调用,外部世界会感觉他们正在与原始对象交互,而不是代理。

JDK 动态代理和 CGLIB 代理

Spring AOP 中的代理可以通过两种方式创建:

  • JDK 代理(动态代理):JDK 代理通过实现目标对象的接口并委托方法调用来创建新的代理对象

  • CGLIB 代理:CGLIB 代理通过扩展目标对象并委托方法调用来创建新的代理对象

让我们看看这些代理机制以及它们在下表中的区别:

JDK 代理 CGLIB 代理
它内置在 JDK 中。 它是一个自定义开发的库。
JDK 代理在接口上工作。 CGLIB 代理在子类上工作。当接口不存在时使用。
它将代理所有接口。 当方法和类都是 final 时无法工作。

从 Spring 3.2 开始,CGLIB 库已经打包到 Spring Core 中,因此在我们的应用程序中不需要单独包含这个库。

从 Spring 4.0 开始,代理对象的构造函数将不会被调用两次,因为 CGLIB 代理实例将通过 Objenesis 创建。

默认情况下,如果目标对象的类实现了接口,则 Spring 将尝试使用 JDK 动态代理;如果目标对象的类没有实现任何接口,则 Spring 将使用 CGLIB 库创建代理。

如果目标对象的类实现了一个接口,并且作为一个具体类注入到另一个 bean 中,那么 Spring 将抛出异常:NoSuchBeanDefinitionException。解决这个问题的方法要么通过接口注入(这是最佳实践),要么用Scope(proxyMode=ScopedProxyMode.TARGET_CLASS)注解注入。然后 Spring 将使用 CGLIB 代理创建代理对象。这个配置禁用了 Spring 使用 JDK 代理。Spring 将始终扩展具体类,即使注入了一个接口。CGLIB 代理使用装饰器模式通过创建代理来将建议编织到目标对象中:

JDK 动态代理和 CGLIB 代理

创建代理将能够将所有调用委托给拦截器(建议)。但是,一旦方法调用到达目标对象,目标对象内部的任何方法调用都不会被拦截。因此,对象引用内的任何方法调用都不会导致任何建议执行。为了解决这个问题,要么重构代码,使直接自我调用不会发生,要么使用 AspectJ 编织。为了在 Spring 中解决这个问题,我们需要将expose a proxy属性设置为 true,并使用AopContext.currentProxy()进行自我调用。

Spring 建议尽可能使用 JDK 代理。因此,尽量在应用程序的几乎所有地方实现抽象层,这样当接口可用且我们没有明确设置为仅使用 CGLIB 代理时,将应用 JDK 代理。

ProxyFactoryBean

Spring 提供了一种经典的方式来手动创建对象的代理,使用ProxyFactoryBean,它将创建一个 AOP 代理包装目标对象。ProxyFactoryBean提供了一种设置建议和建议者的方法,最终合并到 AOP 代理中。从 Spring 中所有 AOP 代理工厂继承的org.springframework.aop.framework.ProxyConfig超类的关键属性如下:

  • proxyTargetClass:如果为 true,则仅使用 CGLIB 创建代理。如果未设置,则如果目标类实现了接口,则使用 JDK 代理创建代理;否则,将使用 CGLIB 创建代理。

  • optimize:对于 CGLIB 代理,这指示代理应用一些激进的优化。目前,JDK 代理不支持这一点。这需要明智地使用。

  • 冻结:如果代理设置为冻结,则不允许对配置进行更改。当我们不希望调用者在代理创建后修改代理时,这是很有用的。这用于优化。此属性的默认值为false

  • exposeProxy:将此属性设置为 true 确定当前代理是否应该暴露给ThreadLocal。如果暴露给ThreadLocal,则目标可以使用AopContext.currentProxy()方法进行方法的自我调用。

ProxyFactoryBean 的作用

我们将定义一个常规的 Spring bean 作为目标 bean,比如TransferService,然后,使用ProxyFactoryBean,我们将创建一个代理,该代理将被我们的应用程序访问。为了对TransferServicetransfer方法进行建议,我们将使用AspectJExpressionPointcut设置切点表达式,并创建拦截器,然后将其设置到DefaultPointcutAdvisor中创建建议者。

目标对象或 bean 如下:

public class TransferServiceImpl implements TransferService {
  private static final Logger LOGGER =     
  Logger.getLogger(TransferServiceImpl.class);

  @Override
  public boolean transfer(Account source, Account dest, Double amount) {
    // transfer amount from source account to dest account
    LOGGER.info("Transferring " + amount + " from " + 
    source.getAccountName() + " 
    to " +   dest.getAccountName());
    ((TransferService)
    (AopContext.currentProxy())).checkBalance(source);
    return true;
  }

  @Override
  public double checkBalance(Account a) {
    return 0;
  }
}

以下代码是方法拦截器或建议:

public class TransferInterceptor implements MethodBeforeAdvice{

   private static final Logger LOGGER =  
   Logger.getLogger(TransferInterceptor.class);

 @Override
 public void before(Method arg0, Object[] arg1, Object arg2) throws   
 Throwable {
    LOGGER.info("transfer intercepted");
 }
}

Spring 配置如下:

@Configuration
public class ProxyFactoryBeanConfig {

  @Bean
  public Advisor transferServiceAdvisor() {
      AspectJExpressionPointcut pointcut = new 
      AspectJExpressionPointcut();
      pointcut.setExpression("execution(* 
      com.packt.springhighperformance.ch03.bankingapp.service
      .TransferService.checkBalance(..))");
      return new DefaultPointcutAdvisor(pointcut, new 
      TransferInterceptor());
  }

  @Bean
  public ProxyFactoryBean transferService(){
    ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
    proxyFactoryBean.setTarget(new TransferServiceImpl());
    proxyFactoryBean.addAdvisor(transferServiceAdvisor());
    proxyFactoryBean.setExposeProxy(true);
    return proxyFactoryBean;
  }
}

在前面的代码示例中,我们没有单独定义TransferService作为 Spring bean。我们创建了TransferService的匿名 bean,然后使用ProxyFactoryBean创建了它的代理。这样做的好处是TransferService类型只有一个对象,没有人可以获得未经建议的对象。这也减少了如果我们想要使用 Spring IoC 将此 bean 连接到任何其他 bean 时的歧义。

使用ProxyFactoryBean,我们可以配置 AOP 代理,提供了编程方法的所有灵活性,而不需要我们的应用进行 AOP 配置。

最好使用声明性的代理配置方法,而不是编程方法,除非我们需要在运行时执行操作或者想要获得细粒度的控制。

性能 JDK 动态代理与 CGLIB 代理

我们了解了代理的用途。根据 GoF 书籍《设计模式:可复用面向对象软件的元素》,代理是另一个对象的占位符,用于控制对它的访问。由于代理位于调用对象和真实对象之间,它可以决定是否阻止对真实(或目标)对象的调用,或者在调用目标对象之前执行一些操作。

许多对象关系映射器使用代理模式来实现一种行为,该行为可以防止数据在实际需要之前被加载。有时这被称为延迟加载。Spring 也使用代理来开发一些功能,比如事务管理、安全性、缓存和 AOP 框架。

由于代理对象是在运行时由 JDK 代理或 CGLIB 库创建的额外对象,并位于调用对象和目标对象之间,它将增加对普通方法调用的一些开销。

让我们找出代理对普通方法调用增加了多少开销。

以下片段显示了 CGLIB 代理的 Spring 基于 Java 的配置类:

@EnableAspectJAutoProxy
@Configuration
public class CGLIBProxyAppConfig {

  @Bean
  @Scope(proxyMode=ScopedProxyMode.TARGET_CLASS)
  public TransferService transferService(){
    return new TransferServiceImpl();
  }
}

JDK 代理的 Spring 基于 Java 的配置类如下:

@Configuration
@EnableAspectJAutoProxy
public class JDKProxyAppConfig {

 @Bean
 @Scope(proxyMode=ScopedProxyMode.INTERFACES)
 public TransferService transferService(){
 return new TransferServiceImpl();
 }
}

JUnit 类如下:

public class TestSpringProxyOverhead {
  private static final Logger LOGGER = 
  Logger.getLogger(TestSpringProxyOverhead.class);

  @Test
  public void checkProxyPerformance() {
    int countofObjects = 3000;
    TransferServiceImpl[] unproxiedClasses = new 
    TransferServiceImpl[countofObjects];
    for (int i = 0; i < countofObjects; i++) {
      unproxiedClasses[i] = new TransferServiceImpl();
    }

    TransferService[] cglibProxyClasses = new     
    TransferService[countofObjects];
    TransferService transferService = null;
    for (int i = 0; i < countofObjects; i++) {
      transferService = new 
      AnnotationConfigApplicationContext(CGLIBProxyAppConfig.class)
      .getBean(TransferService.class);
      cglibProxyClasses[i] = transferService;
    }

    TransferService[] jdkProxyClasses = new 
    TransferService[countofObjects];
    for (int i = 0; i < countofObjects; i++) {
      transferService = new 
      AnnotationConfigApplicationContext(JDKProxyAppConfig.class)
      .getBean(TransferService.class);
      jdkProxyClasses[i] = transferService;
    }

    long timeTookForUnproxiedObjects = 
    invokeTargetObjects(countofObjects, 
    unproxiedClasses);
    displayResults("Unproxied", timeTookForUnproxiedObjects);

    long timeTookForJdkProxiedObjects = 
    invokeTargetObjects(countofObjects, 
    jdkProxyClasses);
    displayResults("Proxy", timeTookForJdkProxiedObjects);

    long timeTookForCglibProxiedObjects = 
    invokeTargetObjects(countofObjects, 
    cglibProxyClasses);
    displayResults("cglib", timeTookForCglibProxiedObjects);

  }

  private void displayResults(String label, long timeTook) {
  LOGGER.info(label + ": " + timeTook + "(ns) " + (timeTook / 1000000) 
  + "(ms)");
  }

  private long invokeTargetObjects(int countofObjects, 
  TransferService[] classes) {
    long start = System.nanoTime();
    Account source = new Account(123456, "Account1");
    Account dest = new Account(987654, "Account2");
    for (int i = 0; i < countofObjects; i++) {
      classes[i].transfer(source, dest, 100);
    }
    long end = System.nanoTime();
    long execution = end - start;
    return execution;
  }
}

开销时间根据硬件工具(如 CPU 和内存)而异。以下是我们将获得的输出类型:

2018-02-06 22:05:01 INFO TestSpringProxyOverhead:52 - Unproxied: 155897(ns) 0(ms)
2018-02-06 22:05:01 INFO TestSpringProxyOverhead:52 - Proxy: 23215161(ns) 23(ms)
2018-02-06 22:05:01 INFO TestSpringProxyOverhead:52 - cglib: 30276077(ns) 30(ms)

我们可以使用诸如 Google 的 Caliper(github.com/google/caliper)或Java 微基准测试工具JMH)(openjdk.java.net/projects/code-tools/jmh/)等工具进行基准测试。使用不同的工具和场景进行了许多性能测试,得到了不同的结果。一些测试显示 CGLIB 比 JDK 代理更快,而另一些测试得到了其他结果。如果我们测试 AspectJ,这是本章稍后将讨论的内容,性能仍然优于 JDK 代理和 CGLIB 代理,因为它使用了字节码编织机制而不是代理对象。

这里的问题是我们是否真的需要担心我们看到的开销?答案既是肯定的,也是否定的。我们将讨论这两个答案。

我们不必真正担心开销,因为代理增加的时间微不足道,而 AOP 或代理模式提供的好处很大。我们已经在本章的前几节中看到了 AOP 的好处,比如事务管理、安全性、延迟加载或任何横切的东西,但通过代码简化、集中管理或代码维护。

此外,当我们的应用程序有服务级别协议SLA)以毫秒交付,或者我们的应用程序有非常高的并发请求或负载时,我们还需要担心开销。在这种情况下,每花费一毫秒对我们的应用程序都很重要。但是,我们仍然需要在我们的应用程序中使用 AOP 来实现横切关注点。因此,我们需要在这里注意正确的 AOP 配置,避免不必要的扫描对象以获取建议,配置我们想要建议的确切连接点,并避免通过 AOP 实现细粒度要求。对于细粒度要求,用户可以使用 AspectJ(字节码编织方法)。

因此,经验法则是,使用 AOP 来实现横切关注点并利用其优势。但是,要谨慎实施,并使用正确的配置,不会通过对每个操作应用建议或代理来降低系统性能。

缓存

为了提高应用程序的性能,缓存重操作是不可避免的。Spring 3.1 添加了一个名为caching的优秀抽象层,帮助放弃所有自定义实现的aspects,装饰器和注入到与缓存相关的业务逻辑中的代码。

Spring 使用 AOP 概念将缓存应用于 Spring bean 的方法;我们在本章的AOP 概念部分学习了它。Spring 会创建 Spring bean 的代理,其中方法被注释为缓存。

为了利用 Spring 的缓存抽象层的好处,只需使用@Cacheable注释重的重方法。此外,我们需要通过在配置类上注释@EnableCaching来通知我们的应用程序方法已被缓存。以下是缓存方法的示例:

@Cacheable("accounts")
public Account findAccountById(int accountId){

@Cacheable注释具有以下属性:

  • value:缓存的名称

  • key:每个缓存项的缓存键

  • condition:根据Spring 表达式语言SpEL)表达式的评估来定义是否应用缓存

  • unless:这是另一个用 SpEL 编写的条件,如果为真,则阻止返回值被缓存

以下是 Spring 提供的与缓存相关的其他注释:

  • @CachePut:它将允许方法执行并更新缓存

  • @CacheEvict:它将从缓存中删除陈旧的数据

  • @Caching:它允许您在同一方法上组合多个注释@Cacheable@CachePut@CacheEvict

  • @CacheConfig:它允许我们在整个类上注释,而不是在每个方法上重复

我们可以在检索数据的方法上使用@Cacheable,并在执行插入以更新缓存的方法上使用@CachePut。代码示例如下:

@Cacheable("accounts" key="#accountId")
public Account findAccountById(int accountId){

@CachePut("accounts" key="#account.accountId")
public Account createAccount(Account account){

对方法进行注释以缓存数据不会存储数据;为此,我们需要实现或提供CacheManager。Spring 默认情况下在org.springframework.cache包中提供了一些缓存管理器,其中之一是SimpleCacheManagerCacheManager代码示例如下:

@Bean
public CacheManager cacheManager() {
  CacheManager cacheManager = new SimpleCacheManager();
  cacheManager.setCaches(Arrays.asList(new     
  ConcurrentMapCache("accounts"));
  return cacheManager;
}

Spring 还提供支持,以集成以下第三方缓存管理器:

  • EhCache

  • Guava

  • Caffeine

  • Redis

  • Hazelcast

  • 您的自定义缓存

AOP 方法分析

应用程序可以有许多业务方法。由于一些实现问题,一些方法需要时间,我们希望测量这些方法花费了多少时间,我们可能还想分析方法参数。Spring AOP 提供了一种执行方法分析的方法,而不触及业务方法。让我们看看如何做到这一点。

PerformanceMonitorInterceptor

让我们看看如何对我们的方法执行进行分析或监视。这是通过 Spring AOP 提供的PerformanceMonitorInterceptor类的简单选项来完成的。

正如我们所了解的,Spring AOP 允许在应用程序中通过拦截一个或多个方法的执行来定义横切关注点,以添加额外功能,而不触及核心业务类。

Spring AOP 中的PerformanceMonitorInterceptor类是一个拦截器,可以绑定到任何自定义方法以在同一时间执行。该类使用StopWatch实例来记录方法执行的开始和结束时间。

让我们监视TransferServicetransfer方法。以下是TransferService的代码:

public class TransferServiceImpl implements TransferService {

  private static final Logger LOGGER = 
  LogManager.getLogger(TransferServiceImpl.class);

  @Override
  public boolean transfer(Account source, Account dest, int amount) {
    // transfer amount from source account to dest account
    LOGGER.info("Transferring " + amount + " from " + 
    source.getAccountName() + " 
    to " + dest.getAccountName());
    try {
      Thread.sleep(5000);
    } catch (InterruptedException e) {
      LOGGER.error(e);
    }
    return true;
  }
}

以下代码是@Pointcut,用于使用 Spring 拦截器监视建议方法:

@Aspect 
public class TransferMonitoringAspect {

    @Pointcut("execution(*          
    com.packt.springhighperformance.ch03.bankingapp.service
    .TransferService.transfer(..))")
    public void transfer() { }
}

以下代码是 advisor 类:

public class PerformanceMonitorAdvisor extends DefaultPointcutAdvisor {

 private static final long serialVersionUID = -3049371771366224728L;

 public PerformanceMonitorAdvisor(PerformanceMonitorInterceptor 
 performanceMonitorInterceptor) {
 AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
 pointcut.setExpression(
 "com.packt.springhighperformance.ch03.bankingapp.aspect.TransferMonito  ringAspect.transfer()");
 this.setPointcut(pointcut);
 this.setAdvice(performanceMonitorInterceptor);
 }
}

以下代码是 Spring Java 配置类:

@EnableAspectJAutoProxy
@Configuration
public class PerformanceInterceptorAppConfig {
  @Bean
  public TransferService transferService() {
    return new TransferServiceImpl();
  }

  @Bean
  public PerformanceMonitorInterceptor performanceMonitorInterceptor() {
    return new PerformanceMonitorInterceptor(true);
  }

  @Bean
  public TransferMonitoringAspect transferAspect() {
    return new TransferMonitoringAspect();
  }

  @Bean
  public PerformanceMonitorAdvisor performanceMonitorAdvisor() {
    return new 
    PerformanceMonitorAdvisor(performanceMonitorInterceptor());
  }
}

Pointcut 表达式标识我们想要拦截的方法。我们已经将PerformanceMonitorInterceptor定义为一个 bean,然后创建了PerformanceMonitorAdvisor来将切入点与拦截器关联起来。

在我们的Appconfig中,我们使用@EnableAspectJAutoProxy注解来为我们的 bean 启用 AspectJ 支持,以自动创建代理。

要使PerformanceMonitorInterceptor起作用,我们需要将目标对象TransferServiceImpl的日志级别设置为TRACE级别,因为这是它记录消息的级别。

对于每次执行transfer方法,我们将在控制台日志中看到TRACE消息:

2018-02-07 22:14:53 TRACE TransferServiceImpl:222 - StopWatch 'com.packt.springhighperformance.ch03.bankingapp.service.TransferService.transfer': running time (millis) = 5000

自定义监视拦截器

PerformanceMonitorInterceptor是监视我们方法执行时间的一种非常基本和简单的方式。然而,大多数情况下,我们需要更加受控的方式来监视方法及其参数。为此,我们可以通过扩展AbstractMonitoringInterceptor或编写环绕建议或自定义注解来实现我们的自定义拦截器。在这里,我们将编写一个扩展AbstractMonitoringInterceptor的自定义拦截器。

让我们扩展AbstractMonitoringInterceptor类,并重写invokeUnderTrace方法来记录方法的startend和持续时间。如果方法执行时间超过5毫秒,我们还可以记录警告。以下是自定义监视拦截器的代码示例:

public class CustomPerformanceMonitorInterceptor extends AbstractMonitoringInterceptor {

    private static final long serialVersionUID = -4060921270422590121L;
    public CustomPerformanceMonitorInterceptor() {
    }

    public CustomPerformanceMonitorInterceptor(boolean 
    useDynamicLogger) {
            setUseDynamicLogger(useDynamicLogger);
    }

    @Override
    protected Object invokeUnderTrace(MethodInvocation invocation, Log 
    log) 
      throws Throwable {
        String name = createInvocationTraceName(invocation);
        long start = System.currentTimeMillis();
        log.info("Method " + name + " execution started at:" + new 
        Date());
        try {
            return invocation.proceed();
        }
        finally {
            long end = System.currentTimeMillis();
            long time = end - start;
            log.info("Method "+name+" execution lasted:"+time+" ms");
            log.info("Method "+name+" execution ended at:"+new Date());

            if (time > 5){
                log.warn("Method execution took longer than 5 ms!");
            } 
        }
    }
}

在基本的PerformanceMonitorInterceptor中看到的每一步都是相同的,只是用CustomPerformanceMonitorInterceptor替换PerformanceMonitorInterceptor

生成以下输出:

2018-02-07 22:23:44 INFO TransferServiceImpl:32 - Method com.packt.springhighperformance.ch03.bankingapp.service.TransferService.transfer execution lasted:5001 ms
2018-02-07 22:23:44 INFO TransferServiceImpl:33 - Method com.packt.springhighperformance.ch03.bankingapp.service.TransferService.transfer execution ended at:Wed Feb 07 22:23:44 EST 2018
2018-02-07 22:23:44 WARN TransferServiceImpl:36 - Method execution took longer than 5 ms!

Spring AOP 与 AspectJ

到目前为止,我们已经看到了使用代理模式和运行时织入的 AOP。现在让我们看看编译时和加载时织入的 AOP。

什么是 AspectJ?

正如我们从本章的开头所知,AOP 是一种编程范式,通过将横切关注点的实现分离来帮助解耦我们的代码。AspectJ 是 AOP 的原始实现,它使用 Java 编程语言的扩展来实现关注点和横切关注点的编织。

为了在我们的项目中启用 AspectJ,我们需要 AspectJ 库,AspectJ 根据其用途提供了不同的库。可以在mvnrepository.com/artifact/org.aspectj找到所有的库。

在 AspectJ 中,Aspects将在扩展名为.aj的文件中创建。以下是TransferAspect.aj文件的示例:

public aspect TransferAspect {
    pointcut callTransfer(Account acc1, Account acc2, int amount) : 
     call(public * TransferService.transfer(..));

    boolean around(Account acc1, Account acc2, int amount) : 
      callTransfer(acc1, acc2,amount) {
        if (acc1.balance < amount) {
            return false;
        }
        return proceed(acc1, acc2,amount);
    }
}

要启用编译时织入,当我们既有aspect代码又有我们想要织入aspects的代码时,使用 Maven 插件如下:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.11</version>
    <configuration>
        <complianceLevel>1.8</complianceLevel>
        <source>1.8</source>
        <target>1.8</target>
        <showWeaveInfo>true</showWeaveInfo>
        <verbose>true</verbose>
        <Xlint>ignore</Xlint>
        <encoding>UTF-8 </encoding>
    </configuration>
    <executions>
        <execution>
            <goals>
                <!-- use this goal to weave all your main classes -->
                <goal>compile</goal>
                <!-- use this goal to weave all your test classes -->
                <goal>test-compile</goal>
            </goals>
        </execution>
    </executions>
</plugin>

要执行编译后织入,当我们想要织入现有的类文件和 JAR 文件时,使用 Mojo 的 AspectJ Maven 插件如下。我们引用的 artifact 或 JAR 文件必须在 Maven 项目的<dependencies/>中列出,并在 AspectJ Maven 插件的<configuration>中列出为<weaveDependencies/>。以下是如何定义织入依赖项的 Maven 示例:

<configuration>
    <weaveDependencies>
        <weaveDependency> 
            <groupId>org.agroup</groupId>
            <artifactId>to-weave</artifactId>
        </weaveDependency>
        <weaveDependency>
            <groupId>org.anothergroup</groupId>
            <artifactId>gen</artifactId>
        </weaveDependency>
    </weaveDependencies>
</configuration>

要执行加载时织入LTW),当我们想要推迟我们的织入直到类加载器加载类文件时,我们需要一个织入代理;使用 Maven 插件如下:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.20.1</version>
    <configuration>
        <argLine>
            -javaagent:"${settings.localRepository}"/org/aspectj/
            aspectjweaver/${aspectj.version}/
            aspectjweaver-${aspectj.version}.jar
        </argLine>
        <useSystemClassLoader>true</useSystemClassLoader>
        <forkMode>always</forkMode>
    </configuration>
</plugin>

对于 LTW,它在META-INF文件夹下的类路径中查找aop.xml。文件包含如下的aspectweaver标签:

<aspectj>
    <aspects>
        <aspect name="com.packt.springhighperformance.ch3.bankingapp.
        aspectj.TransferAspect"/>
        <weaver options="-verbose -showWeaveInfo">
            <include         
            within="com.packt.springhighperformance.ch3.bankingapp
            .service.impl.TransferServiceImpl"/>
        </weaver>
    </aspects>
</aspectj>

这只是一个关于如何在项目中启用 AspectJ 的介绍。

Spring AOP 和 AspectJ 之间的区别

让我们来看看 Spring AOP(运行时织入)和 AspectJ(编译时和 LTW)之间的区别。

能力和目标

Spring AOP 提供了一个简单的 AOP 实现,使用代理模式和装饰器模式来实现横切关注点。它不被认为是一个完整的 AOP 解决方案,Spring 可以应用于由 Spring 容器管理的 bean。

AspectJ 是最初的 AOP 技术,旨在提供完整的 AOP 解决方案。它比 Spring AOP 更健壮,但也更复杂。AspectJ 的好处是可以应用于所有领域对象。

织入

Spring AOP 和 AspectJ 都使用不同类型的织入,根据它们的织入机制,它们在性能和易用性方面的行为是不同的。

为了在应用程序执行期间执行我们的aspects的运行时织入,Spring 使用 JDK 动态代理或 CGLIB 代理创建目标对象的代理,这是我们之前讨论过的。

与 Spring AOP 的运行时织入相反,AspectJ 在编译时或类加载时执行织入。我们已经在前面的部分看到了不同类型的 AspectJ 织入。

连接点

由于 Spring AOP 创建目标类或对象的代理来应用横切关注点(Aspects),它需要对目标类或对象进行子类化。正如我们已经知道的,通过子类化,Spring AOP 无法在最终或静态的类或方法上应用横切关注点。

另一方面,AspectJ 通过字节码织入将横切关注点编织到实际代码中,因此它不需要对目标类或对象进行子类化。

简单性

在 Spring AOP 中,Aspects的运行时织入将由容器在启动时执行,因此它与我们的构建过程无缝集成。

另一方面,在 AspectJ 中,除非我们在后期编译或在 LTW 中执行此操作,否则我们必须使用额外的编译器(ajc)。因此,Spring 比 AspectJ 更简单、更易管理。

使用 Spring AOP,我们无法使用或应用 AOP 的全部功能,因为 Spring AOP 是基于代理的,只能应用于 Spring 管理的 bean。

AspectJ 基于字节码织入,这意味着它修改了我们的代码,因此它使我们能够在应用程序的任何 bean 上使用 AOP 的全部功能。

性能

从性能的角度来看,编译时织入比运行时织入更快。Spring AOP 是基于代理的框架,因此它在运行时为代理创建额外的对象,并且每个aspect有更多的方法调用,这对性能产生负面影响。

另一方面,AspectJ 在应用程序启动之前将aspects编织到主代码中,因此没有额外的运行时开销。互联网上有可用的基准测试表明,AspectJ 比 Spring AOP 快得多。

并不是说一个框架比另一个更好。选择将基于需求和许多不同因素,例如开销、简单性、可管理性/可维护性、复杂性和学习曲线。如果我们使用较少的aspects,并且除了 Spring bean 或方法执行之外没有应用aspect的需求,那么 Spring AOP 和 AspectJ 之间的性能差异是微不足道的。我们也可以同时使用 AspectJ 和 Spring AOP 来实现我们的需求。

Spring 中的 AspectJ

Spring 提供了小型库,以将 AspectJ aspects集成到 Spring 项目中。这个库被命名为spring-aspects.jar。正如我们从之前的讨论中了解到的,Spring 只允许在 Spring bean 上进行依赖注入或 AOP 建议。使用这个小库的 Spring 的 AspectJ 支持,我们可以为 Spring 驱动的配置启用在容器外创建的任何对象。只需用@Configurable注释外部对象。用@Configurable注释非 Spring bean 将需要spring-aspects.jar中的AnnotationBeanConfigurerAspect。Spring 需要的AnnotationBeanConfigurerAspect配置可以通过用@EnableSpringConfigured注释我们的配置 Java 配置类来完成。

Spring 提供了一种更精细的方式来启用加载时织入LTW),通过启用每个类加载器基础。这在将大型或多个应用程序部署到单个 JVM 环境时提供了更精细的控制。

要在 Spring 中使用 LTW,我们需要像在AOP 概念部分中实现的那样实现我们的aspect或建议,并且根据 AspectJ 概念,我们需要在META-INF文件夹中创建aop.xml,如下所示:

<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
    <weaver>
        <!-- only weave classes in our application-specific packages --
        >
        <include within="com.packt.springhighperformance.ch3.bankingapp
        .service.impl.TransferServiceImpl"/>
        <include within="com.packt.springhighperformance.ch3.bankingapp
        .aspects.TransferServiceAspect"/>
    </weaver>
    <aspects>
        <!-- weave in just this aspect -->
        <aspect name="com.packt.springhighperformance.ch3.bankingapp
        .aspects.TransferServiceAspect"/>
    </aspects>
</aspectj>

我们需要做的最后一件事是用@EnableLoadTimeWeaving注释我们基于 Java 的 Spring 配置。我们需要在服务器启动脚本中添加-javaagent:path/to/org.springframework.instrument-{version}.jar

AOP 最佳编程实践

我们已经了解了为什么需要在我们的应用程序中使用 AOP。我们详细了解了它的概念以及如何使用它。让我们看看在我们的应用程序中使用 AOP 时应该遵循哪些最佳实践。

切入点表达式

我们在 AOP 方面学习了切入点。现在让我们看看在使用切入点时应该注意什么:

  • Spring 与 AspectJ 在编译期间处理切入点,并尝试匹配和优化匹配性能。然而,检查代码和匹配(静态或动态)将是一个昂贵的过程。因此,为了实现最佳性能,要三思而后行,尽量缩小我们想要实现的搜索或匹配标准。

  • 我们在本章中早些时候学习的所有指示符分为三类:

  • 方法签名模式:executiongetsetcallhandler

  • 类型签名模式:withinwithincode

  • 上下文签名模式:thistarget@annotation

  • 为了实现良好的性能,编写切入点应至少包括方法和类型签名模式。如果只使用方法或类型模式进行匹配,可能不会起作用;然而,始终建议将方法和类型签名结合在一起。类型签名非常快速,通过快速排除无法进一步处理的连接点,缩小了搜索空间。

  • 在空方法上声明切入点,并通过其空方法名称引用这些切入点(命名切入点),这样在表达式发生任何更改时,我们只需要在一个地方进行更改。

  • 建议声明小命名的切入点,并通过名称组合它们来构建复杂的切入点。按名称引用切入点将遵循默认的 Java 方法可见性规则。以下是定义小切入点并将它们连接的代码示例:

@Pointcut("execution(public * *(..))")
private void anyPublicMethod() {}

@Pointcut("within(com.packt.springhighperformance.ch3.bankingapp.TransferService..*)")
private void transfer() {}

@Pointcut("anyPublicMethod() && transfer()")
private void transferOperation() {}

  • 尽量在切入点不共享时为其创建匿名 bean,以避免应用程序直接访问。

  • 尽量使用静态切入点,其中不需要匹配参数。这些更快,并且在首次调用方法时由 Spring 缓存。动态切入点成本高,因为它们在每次方法调用时进行评估,因为无法进行缓存,参数会有所不同。

建议顺序

现在我们知道如何编写建议以及如何创建Aspect。让我们看看建议顺序如何帮助我们在同一连接点上有多个建议时优先考虑我们的建议:

  • 假设我们在不同的 aspects 中编写了两个 before 或 after advice,并且两者都希望在相同的连接点运行。在这种情况下,advice 的执行顺序将基于在类执行中哪个 aspect 先出现。为了避免这种情况并依次应用我们的 advice,Spring 提供了一种方法来指定执行顺序,即通过一个 aspect 实现Ordered接口或应用@Order注解。顺序的值越低,优先级越高。

  • 在声明 advice 时,始终使用最不强大的 advice 形式;例如,如果一个简单的 before advice 可以实现我们的要求,我们就不应该使用 around advice。

AOP 代理的最佳实践

我们了解了 AOP 代理以及 AOP 代理的工作原理。我们了解了 Spring AOP 中不同类型的代理。在实现 Spring 中的 AOP 代理时,应遵循以下最佳实践:

  • 除非我们需要在运行时执行操作或者想要对代理进行细粒度控制,否则使用声明式代理配置而不是编程方法。

  • Spring 在可能的情况下建议使用 JDK 动态代理而不是 CGLIB 代理。如果我们从头开始构建我们的应用程序,并且没有要创建第三方 API 的代理的要求,实现抽象层以松散耦合实现,使用接口并让 Spring 使用 JDK 动态代理机制来创建代理。

  • 在 CGLIB 代理的情况下,确保方法不是final,因为final方法无法被覆盖,因此也无法被 advised。

  • 根据 Spring 的说法,aspects本身不可能成为其他aspects的 advice 目标。对于这种情况有解决方法;将aspect方法移动到一个新的 Spring bean 上,并用@Component进行注释,将这个新的 Spring bean 自动装配到aspect,然后调用被 advised 的方法。MethodProfilingAspect是在com.packt.springhighperformance.ch3.bankingapp下定义一个切入点的aspect

@Aspect
public class MethodProfilingAspect {

  @Around("execution(* 
  com.packt.springhighperformance.ch3.bankingapp.*.*(..))")
  public Object log(ProceedingJoinPoint joinPoint){
    System.out.println("Before 
    Around"+joinPoint.getTarget().getClass().getName());
    Object retVal = null;
    try {
       retVal = joinPoint.proceed();
    } catch (Throwable e) {
      e.printStackTrace();
    }
    System.out.println("After 
    Around"+joinPoint.getTarget().getClass().getName());
    return retVal;
  }
  • 以下的ValidatingAspect是在com.packt.springhighperformance.ch3.bankapp包下定义的aspect,但是MethodProfilingAspect不建议调用validate方法:
@Aspect
public class ValidatingAspect {

 @Autowired
 private ValidateService validateService;

 @Before("execution(*   
 com.packt.springhighperformance.ch3.bankingapp.TransferService.tran
 sfe  r(..))")
 public void validate(JoinPoint jp){
 validateService.validateAccountNumber();
 }
}
  • 通过创建一个带有@Component注解的单独类并实现validate方法来解决这个问题。这个类将是一个 Spring 管理的 bean,并且会被 advised:
@Component
public class ValidateDefault{

  @Autowired
  private ValidateService validateService;
  public void validate(JoinPoint jp){
        validateService.validateAccountNumber();
    }
}
  • 以下的ValidatingAspect代码注入了ValidateDefault Spring bean 并调用了validate方法:
@Aspect
public class ValidatingAspect {

 @Autowired
 private ValidateDefault validateDefault;

 @Before("execution(* com.packt.springhighperformance.ch3.bankingapp.TransferService.transfer(..))")
 public void validate(JoinPoint jp){

 validateDefault.validate(jp);
 }
}

永远不要通过 AOP 实现细粒度的要求,也不要对 Spring 管理的 bean 类使用@Configurable,否则会导致双重初始化,一次是通过容器,一次是通过 aspect。

缓存

我们已经看到了如何通过缓存来提高应用程序的性能。在 Spring 中实现缓存时应遵循以下最佳实践:

  • Spring 缓存注解应该用在具体类上,而不是接口上。如果选择使用proxy-target-class="true",缓存将不起作用,因为接口的 Java 注解无法继承。

  • 尽量不要在同一个方法上同时使用@Cacheable@CachePut

  • 不要缓存非常低级的方法,比如 CPU 密集型和内存计算。在这些情况下,Spring 的缓存可能过度。

总结

在本章中,我们看了 Spring AOP 模块。AOP 是一种强大的编程范式,它补充了面向对象编程。AOP 帮助我们将横切关注点与业务逻辑解耦,并在处理业务需求时只关注业务逻辑。解耦的横切关注点有助于实现可重用的代码。

我们学习了 AOP 的概念、术语以及如何实现建议。我们了解了代理和 Spring AOP 如何使用代理模式实现。我们学习了在使用 Spring AOP 时应遵循的最佳实践,以实现更好的质量和性能。

在下一章中,我们将学习关于 Spring MVC。Spring Web MVC 提供了一个基于 MVC 的 Web 框架。使用 Spring Web MVC 作为 Web 框架使我们能够开发松耦合的 Web 应用程序,并且可以编写测试用例而不使用请求和响应对象。我们将看到如何优化 Spring MVC 实现,以利用异步方法特性、多线程和身份验证缓存来实现更好的结果。

第四章:Spring MVC 优化

在上一章中,我们学习了 Spring 面向切面编程AOP)模块,AOP 概念,其各种术语,以及如何实现建议。我们还了解了代理概念及其使用代理模式的实现。我们通过最佳实践来实现 Spring AOP 的质量和性能。

Spring MVC 现在是最流行的 Java Web 应用程序框架。它由 Spring 自身提供。Spring Web MVC 有助于开发灵活和松散耦合的基于 Web 的应用程序。Spring MVC 遵循模型-视图-控制器MVC)模式,它将输入逻辑、业务逻辑和表示逻辑分开,同时提供组件之间的松散耦合。Spring MVC 模块允许我们在 Web 应用程序中编写测试用例而不使用请求和响应对象。因此,它消除了在企业应用程序中测试 Web 组件的开销。Spring MVC 还支持多种新的视图技术,并允许扩展。Spring MVC 为控制器、视图解析器、处理程序映射和 POJO bean 提供了清晰的角色定义,使得创建 Java Web 应用程序变得简单。

在本章中,我们将学习以下主题:

  • Spring MVC 配置

  • Spring 异步处理,@Async注解

  • 使用 Spring Async 的CompletableFuture

  • Spring 安全配置

  • 认证缓存

  • 使用 Spring Security 进行快速和无状态的 API 身份验证

  • 使用 JMX 监视和管理 Tomcat

  • Spring MVC 性能改进

Spring MVC 配置

Spring MVC 架构设计了一个前端控制器 Servlet,即DispatcherServlet,它是前端控制器模式的实现,并充当所有 HTTP 请求和响应的入口点。DispatcherServlet可以使用 Java 配置或部署描述符文件web.xml进行配置和映射。在进入配置部分之前,让我们了解 Spring MVC 架构的流程。

Spring MVC 架构

在 Spring MVC 框架中,有多个核心组件来维护请求和响应执行的流程。这些组件被清晰地分开,并且具有不同的接口和实现类,因此可以根据需求使用。这些核心组件如下:

组件 摘要
DispatcherServlet 它作为 Spring MVC 框架的前端控制器,负责 HTTP 请求和响应的生命周期。
HandlerMapping 当请求到来时,这个组件负责决定哪个控制器将处理 URL。
Controller 它执行业务逻辑并映射ModelAndView中的结果数据。
ModelAndView 它以执行结果和视图对象的形式保存模型数据对象。
ViewResolver 它决定要呈现的视图。
View 它显示来自模型对象的结果数据。

以下图表说明了 Spring MVC 架构中前面组件的流程:

Spring MVC 架构

让我们了解架构的基本流程:

  1. 当传入的请求到来时,它被前端控制器DispatcherServlet拦截。在拦截请求后,前端控制器找到适当的HandlerMapping

  2. HandlerMapping将客户端请求调用映射到适当的Controller,根据配置文件或注解Controller列表,并将Controller信息返回给前端控制器。

  3. DispatcherServlet请求分派到适当的Controller

  4. Controller执行在Controller方法下定义的业务逻辑,并将结果数据以ModelAndView的形式返回给前端控制器。

  5. 前端控制器根据ModelAndView中的值获取视图名称并将其传递给ViewResolver以根据配置的视图解析器解析实际视图。

  6. 视图使用模型对象来呈现屏幕。输出以HttpServletResponse的形式生成并传递给前端控制器。

  7. 前端控制器将响应发送回 Servlet 容器,以将输出发送回用户。

现在,让我们了解 Spring MVC 配置方法。Spring MVC 配置可以通过以下方式进行设置:

  • 基于 XML 的配置

  • 基于 Java 的配置

在使用上述方法进行配置之前,让我们定义设置 Spring MVC 应用程序所涉及的步骤:

  1. 配置前端控制器

  2. 创建 Spring 应用程序上下文

  3. 配置ViewResolver

基于 XML 的配置

在基于 XML 的配置中,我们将使用 XML 文件来进行 Spring MVC 配置。让我们按照上述步骤继续进行配置。

配置前端控制器

要在基于 XML 的配置中配置前端控制器 ServletDispatcherServlet,我们需要在web.xml文件中添加以下 XML 代码:

  <servlet>
    <servlet-name>spring-mvc</servlet-name>
    <servlet-class>
      org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/spring-mvc-context.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>spring-mvc</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

在上述 XML 代码中,我们首先配置了DispatcherServlet。然后,我们提到了上下文配置位置/WEB-INF/spring-mvc-context.xml。我们将load-on-startup值设置为1,因此 Servlet 容器将在启动时加载此 Servlet。在第二部分中,我们定义了servlet-mapping标签,将 URL/映射到DispatcherServlet。现在,我们将在下一步中定义 Spring 应用程序上下文。

DispatcherServlet配置下配置load-on-startup元素是一个好习惯,以便在集群环境中,如果 Spring 没有启动并且一旦部署就会有大量的调用命中您的 Web 应用程序,您可能会面临超时问题。

创建 Spring 应用程序上下文

web.xml中配置DispatcherServlet之后,让我们继续创建一个 Spring 应用程序上下文。为此,我们需要在spring-mvc-context.xml文件中添加以下 XML 代码:

<beans>
<!-- Schema definitions are skipped. -->
<context:component-scan base-            package="com.packt.springhighperformance.ch4.controller" />
<mvc:annotation-driven />
</beans>

在上述 XML 代码中,我们首先为com.packt.springhighperformance.ch4.controller包定义了一个组件扫描标签<context:component-scan />,以便所有的 bean 和控制器都能被创建和自动装配。

然后,我们使用了<mvc:annotation-driven />来自动注册不同的 bean 和组件,包括请求映射、数据绑定、验证和使用@ResponseBody进行自动转换功能。

配置 ViewResolver

要配置ViewResolver,我们需要在spring-mvc-context.xml文件中为InternalResourceViewResolver类指定一个 bean,在<mvc:annotation-driven />之后。让我们这样做:

<beans>
<!-- Schema definitions are skipped. -->
<context:component-scan base- package="com.packt.springhighperformance.ch4.controller" />
<mvc:annotation-driven />

<bean
 class="org.springframework.web.servlet.view.InternalResourceViewResolv  er">
    <property name="prefix">
      <value>/WEB-INF/views/</value>
    </property>
    <property name="suffix">
      <value>.jsp</value>
    </property>
  </bean>
</beans>

在配置ViewResolver之后,我们将创建一个Controller来测试配置。但是,在继续之前,让我们看看基于 Java 的配置。

基于 Java 的配置

对于基于 Java 的 Spring MVC 配置,我们将按照与基于 XML 的配置相同的步骤进行。在基于 Java 的配置中,所有配置都将在 Java 类下完成。让我们按照顺序进行。

配置前端控制器

在 Spring 5.0 中,有三种方法可以通过实现或扩展以下三个类来以编程方式配置DispatcherServlet

  • WebAppInitializer 接口

  • AbstractDispatcherServletInitializer 抽象类

  • AbstractAnnotationConfigDispatcherServletInitializer 抽象类

我们将使用AbstractDispatcherServletInitializer类,因为它是使用基于 Java 的 Spring 配置的应用程序的首选方法。它是首选的,因为它允许我们启动一个 Servlet 应用程序上下文,以及一个根应用程序上下文。

我们需要创建以下类来配置DispatcherServlet

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class SpringMvcWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return null;
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class[] { SpringMvcWebConfig.class };
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] { "/" };
  }
}

前面的类代码等同于我们在基于 XML 的配置部分创建的web.xml文件配置。在前面的类中,getRootConfigClasses()方法用于指定根应用程序上下文配置类(如果不需要,则为null)。getServletConfigClasses()用于指定 Web 应用程序配置类(如果不需要,则为null)。getServletMappings()方法用于指定DispatcherServlet的 Servlet 映射。首先加载根配置类,然后加载 Servlet 配置类。根配置类将创建一个ApplicationContext,它将作为父上下文,而 Servlet 配置类将创建一个WebApplicationContext,它将作为父上下文的子上下文。

创建一个 Spring 应用程序上下文并配置 ViewResolver

在 Spring 5.0 中,要使用 Java 配置创建 Spring 应用程序上下文并配置ViewResolver,需要在类中添加以下代码:

@Configuration
@EnableWebMvc
@ComponentScan({ "com.packt.springhighperformance.ch4.bankingapp.controller"})
public class SpringMvcWebConfig implements WebMvcConfigurer {

  @Bean
  public InternalResourceViewResolver resolver() {
    InternalResourceViewResolver resolver = new 
    InternalResourceViewResolver();
    resolver.setPrefix("/WEB-INF/views/");
    resolver.setSuffix(".jsp");
    return resolver;
  }

}

在前面的代码中,我们创建了一个类SpringMvcWebConfig,实现了WebMvcConfigurer接口,该接口提供了自定义 Spring MVC 配置的选项。@EnableWebMvc对象启用了 Spring MVC 的默认配置。@ComponentScan对象指定了要扫描控制器的基本包。这两个注解@EnableWebMvc@ComponentScan等同于我们在基于 XML 的配置部分中创建的spring-mvc-context.xml中的<context:component-scan /><mvc:annotation-driven />resolve()方法返回InternalResourceViewResolver,它有助于从预配置的目录中映射逻辑视图名称。

创建一个控制器

现在,让我们创建一个控制器类来映射/home请求,如下所示:

package com.packt.springhighperformance.ch4.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class BankController {

  @RequestMapping(value = "/home")
  public String home() {
    return "home";
  }
}

在前面的代码中,@Controller定义了一个包含请求映射的 Spring MVC 控制器。@RequestMapping(value = "home")对象定义了一个映射 URL/home到一个方法home()。因此,当浏览器发送一个/home请求时,它会执行home()方法。

创建一个视图

现在,让我们在src/main/webapp/WEB-INF/views/home.jsp文件夹中创建一个视图home.jsp,其中包含以下 HTML 内容:

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Spring MVC</title>
</head>
<body>
  <h2>Welcome to Bank</h2>
</body>
</html>

现在,当我们运行这个应用程序时,它将显示以下输出:

在下一节中,我们将学习关于 Spring 异步处理的内容。

Spring 异步处理,@Async 注解

Spring 提供了对异步方法执行的支持。这也可以使用线程来实现,但会使代码更复杂,有时会导致更多的错误和 bug。当我们需要以异步方式执行简单操作时,使用线程来处理会是一个繁琐的过程。有些情况下需要异步执行操作,比如从一台机器发送消息到另一台机器。异步处理的主要优势在于调用者不必等待被调用方法的完成。为了在单独的线程中执行方法,需要使用@Async注解对方法进行注解。

可以通过使用@EnableAsync注解来启用异步处理,以在后台线程池中运行@Async方法。以下是启用异步处理的 Java 配置示例:

@Configuration
@EnableAsync
public class SpringAppAsyncConfig { ... }

异步处理也可以通过使用 XML 配置来启用,如下所示:

<task:executor id="myappexecutor" pool-size="10" />
<task:annotation-driven executor="myappexecutor"/>

@Async 注解模式

@Async注解处理方法有两种模式:

  • 发送并忘记模式

  • 结果检索模式

发送并忘记模式

在这种模式下,方法将配置为void类型,以异步运行:

@Async
public void syncCustomerAccounts() {
    logger.info("Customer accounts synced successfully.");
}

结果检索模式

在这种模式下,方法将配置一个返回类型,通过Future类型来包装结果:

@Service
public class BankAsyncService {

  private static final Logger LOGGER = 
  Logger.getLogger(BankAsyncService.class);

  @Async
    public Future<String> syncCustomerAccount() throws 
    InterruptedException {
    LOGGER.info("Sync Account Processing Started - Thread id: " + 
    Thread.currentThread().getId());

    Thread.sleep(2000);

    String processInfo = String.format("Sync Account Processing 
    Completed - Thread Name= %d, Thread Name= %s", 
    Thread.currentThread().getId(), 
    Thread.currentThread().getName());

    LOGGER.info(processInfo);

    return new AsyncResult<String>(processInfo);
    }
}

Spring 还提供了对AsyncResult类的支持,该类实现了Future接口。它可以用于跟踪异步方法调用的结果。

@Async 注解的限制

@Async注解有以下限制:

  • 方法需要是public,这样它才能被代理

  • 异步方法的自我调用不起作用,因为它会绕过代理直接调用底层方法

线程池执行程序

你可能想知道我们如何声明异步方法将使用的线程池。默认情况下,对于线程池,Spring 将尝试在上下文中找到一个名为TaskExecutor的唯一 bean,或者一个名为TaskExecutorExecutor bean。如果前两个选项都无法解析,Spring 将使用SimpleAsyncTaskExecutor来处理异步方法处理。

然而,有时我们不想为应用程序的所有任务使用相同的线程池。我们可以为每个方法使用不同的线程池,并为每个方法配置不同的线程池。为此,我们只需要将执行器名称传递给每个方法的@Async注解。

为了启用异步支持,@Async注解是不够的;我们需要在配置类中使用@EnableAsync注解。

在 Spring MVC 中,当我们使用AbstractAnnotationConfigDispatcherServletInitializer初始化类配置DispatcherServlet时,它默认启用了isAsyncSupported标志。

现在,我们需要为异步方法调用声明一个线程池定义。在 Spring MVC 基于 Java 的配置中,可以通过在 Spring Web MVC 配置类中覆盖WebMvcConfigurer接口的configureAsyncSupport()方法来实现。让我们按照以下方式覆盖这个方法:

@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
    ThreadPoolTaskExecutor t = new ThreadPoolTaskExecutor();
        t.setCorePoolSize(10);
        t.setMaxPoolSize(100);
        t.setThreadNamePrefix("BankAccountSync");
        t.initialize();
        configurer.setTaskExecutor(t);
}

在前面的方法中,我们通过覆盖configureAsyncSupport()方法配置了线程池执行程序。现在,让我们通过控制器类调用服务类BankAsyncService中创建的异步方法,如下所示:

@Controller
public class BankController {

  private static final Logger LOGGER = Logger.getLogger(BankAsyncService.class);

  @Autowired
  BankAsyncService syncService;

  @RequestMapping(value = "/syncacct")
  @ResponseBody
  public Callable<String> syncAccount() {
    LOGGER.info("Entering in controller");

    Callable<String> asyncTask = new Callable<String>() {

      @Override
      public String call() throws Exception {
        Future<String> processSync = syncService.syncCustomerAccount();
        return processSync.get();
      }
    };

    LOGGER.info("Leaving from controller");
    return asyncTask;
  }
}

在前面的示例中,当我们请求/syncacct时,它将调用syncAccount()并在单独的线程中返回异步方法的结果。

Spring 异步的 CompletableFuture

CompletableFuture类是在 Java 8 中引入的,它提供了一种简单的方式来编写异步、多线程、非阻塞的代码。在 Spring MVC 中,也可以在使用@Async注解的公共方法的控制器、服务和存储库中使用CompletableFutureCompletableFuture实现了Future接口,该接口提供了异步计算的结果。

我们可以通过以下简单方式创建CompletableFuture

CompletableFuture<String> completableFuture = new CompletableFuture<String>();

要获取这个CompletableFuture的结果,我们可以调用CompletableFuture.get()方法。该方法将被阻塞,直到Future完成。为此,我们可以手动调用CompletableFuture.complete()方法来complete Future

completableFuture.complete("Future is completed")

runAsync() - 异步运行任务

当我们想要异步执行后台活动任务,并且不想从该任务中返回任何东西时,我们可以使用CompletableFuture.runAsync()方法。它以Runnable对象作为参数,并返回CompletableFuture<Void>类型。

让我们尝试通过在我们的BankController类中创建另一个控制器方法来使用runAsync()方法,如下所示:

@RequestMapping(value = "/synccust")
  @ResponseBody
  public CompletableFuture<String> syncCustomerDetails() {
    LOGGER.info("Entering in controller");

    CompletableFuture<String> completableFuture = new 
    CompletableFuture<>();
    CompletableFuture.runAsync(new Runnable() {

      @Override
      public void run() {
        try {           
           completableFuture.complete(syncService.syncCustomerAccount()
           .get());
        } catch (InterruptedException | ExecutionException e) {
          completableFuture.completeExceptionally(e);
        }

      }
    }); 
      LOGGER.info("Leaving from controller");
      return completableFuture;
  }

在前面的示例中,当请求使用/synccust路径时,它将在单独的线程中运行syncCustomerAccount(),并在不返回任何值的情况下完成任务。

supplyAsync() - 异步运行任务,带有返回值

当我们想要在异步完成任务后返回结果时,我们可以使用CompletableFuture.supplyAsync()。它以Supplier<T>作为参数,并返回CompletableFuture<T>

让我们通过在我们的BankController类中创建另一个控制器方法来检查supplyAsync()方法,示例如下:

@RequestMapping(value = "/synccustbal")
  @ResponseBody
  public CompletableFuture<String> syncCustomerBalance() {
    LOGGER.info("Entering in controller");

    CompletableFuture<String> completableFuture = 
    CompletableFuture.supplyAsync(new Supplier<String>() {

      @Override
      public String get() {
        try {
          return syncService.syncCustomerBalance().get();
        } catch (InterruptedException | ExecutionException e) {
          LOGGER.error(e);
        }
        return "No balance found";
      }
    }); 
      LOGGER.info("Leaving from controller");
      return completableFuture;
  }

CompletableFuture对象使用全局线程池ForkJoinPool.commonPool()在单独的线程中执行任务。我们可以创建一个线程池并将其传递给runAsync()supplyAsync()方法。

以下是runAsync()supplyAsync()方法的两种变体:

CompletableFuture<Void> runAsync(Runnable runnable)
CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
CompletableFuture<U> supplyAsync(Supplier<U> supplier)
CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

将回调附加到 CompletableFuture

CompletableFuture.get()会阻塞对象,并等待Future任务完成并返回结果。要构建一个异步系统,应该有一个回调,在Future任务完成时自动调用。我们可以使用thenApply()thenAccept()thenRun()方法将回调附加到CompletableFuture

Spring Security 配置

Spring Security 是 Java EE 企业应用程序广泛使用的安全服务框架。在认证级别上,Spring Security 提供了不同类型的认证模型。其中一些模型由第三方提供,一些认证功能集由 Spring Security 自身提供。Spring Security 提供了以下一些认证机制:

  • 基于表单的认证

  • OpenID 认证

  • LDAP 专门用于大型环境

  • 容器管理的认证

  • 自定义认证系统

  • JAAS

让我们看一个示例来在 Web 应用程序中激活 Spring Security。我们将使用内存配置。

配置 Spring Security 依赖项

要在 Web 应用程序中配置 Spring Security,我们需要将以下 Maven 依赖项添加到我们的项目对象模型POM)文件中:

<!-- spring security -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>${spring.framework.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>${spring.framework.version}</version>
</dependency>

为传入请求配置安全过滤器

在 Web 应用程序中实现安全性时,最好验证所有传入的请求。在 Spring Security 中,框架本身查看传入的请求并验证用户以执行操作,基于提供的访问权限。为了拦截 Web 应用程序的所有传入请求,我们需要配置filterDelegatingFilterProxy,它将把请求委托给 Spring 管理的FilterChainProxy

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>
        org.springframework.web.filter.DelegatingFilterProxy
    </filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

基于filter配置,所有请求将通过此filter。现在,让我们配置与安全相关的内容,如身份验证、URL 安全和角色访问。

配置 Spring Security

现在,我们将通过创建 Spring Security 配置类来配置 Spring Security 身份验证和授权,如下所示:

@EnableWebSecurity
public class SpringMvcSecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  PasswordEncoder passwordEncoder;

  @Override
  protected void configure(AuthenticationManagerBuilder auth)       
  throws   
  Exception {
    auth
    .inMemoryAuthentication()
    .passwordEncoder(passwordEncoder)
    .withUser("user").password(passwordEncoder.encode("user@123"))
    .roles("USER")
    .and()
    .withUser("admin").password(passwordEncoder.
    encode("admin@123")        
    ).roles("USER", "ADMIN");
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
     http.authorizeRequests()
    .antMatchers("/login").permitAll()
    .antMatchers("/admin/**").hasRole("ADMIN")
    .antMatchers("/**").hasAnyRole("ADMIN","USER")
    .and().formLogin()
    .and().logout().logoutSuccessUrl("/login").permitAll()
    .and()
    .csrf().disable();
  }
}

让我们理解上述配置:

  • @EnableWebSecurity:它启用了 Spring Security 的 Web 安全支持,并提供了 Spring MVC 集成。

  • WebSecurityConfigurerAdapter:它提供了一组方法,用于启用特定的 Web 安全配置。

  • protected void configure(AuthenticationManagerBuilder auth): 在本示例中,我们使用了内存认证。它可以用于使用auth.jdbcAuthentication()连接到数据库,或者使用auth.ldapAuthentication()连接到轻量级目录访问协议LDAP)。

  • .passwordEncoder(passwordEncoder): 我们使用了密码编码器BCryptPasswordEncoder

  • .withUser("user").password(passwordEncoder.encode("user@123")): 为认证设置用户 ID 和编码密码。

  • .roles("USER"): 为用户分配角色。

  • protected void configure(HttpSecurity http): 用于保护需要安全性的不同 URL。

  • .antMatchers("/login").permitAll(): 允许所有用户访问登录页面。

  • .antMatchers("/admin/**").hasRole("ADMIN"): 允许具有ADMIN角色的用户访问管理员面板。

  • .antMatchers("/**").anyRequest().hasAnyRole("ADMIN", "USER"): 这意味着对于带有"/"的任何请求,您必须使用ADMINUSER角色登录。

  • .and().formLogin(): 它将提供一个默认的登录页面,带有用户名和密码字段。

  • .and().logout().logoutSuccessUrl("/login").permitAll(): 当用户注销时,设置注销成功页面。

  • .csrf().disable(): 默认情况下,跨站请求伪造CSRF)标志是启用的。在这里,我们已经从配置中禁用了它。

添加一个控制器

我们将使用以下BankController类进行 URL 映射:

@Controller
public class BankController {

  @GetMapping("/")
  public ModelAndView home(Principal principal) {
    ModelAndView model = new ModelAndView();
    model.addObject("title", "Welcome to Bank");
    model.addObject("message", "Hi " + principal.getName());
    model.setViewName("index");
    return model;
  }

  @GetMapping("/admin**")
  public ModelAndView adminPage() {
    ModelAndView model = new ModelAndView();
    model.addObject("title", "Welcome to Admin Panel");
    model.addObject("message", "This is secured page - Admin 
    Panel");
    model.setViewName("admin");
    return model;
  }

  @PostMapping("/logout")
  public String logout(HttpServletRequest request, 
  HttpServletResponse 
  response) {
    Authentication auth = 
    SecurityContextHolder.getContext().getAuthentication();
    if (auth != null) {
      new SecurityContextLogoutHandler().logout(request, response, 
      auth);
      request.getSession().invalidate();
    }
    return "redirect:/login";
  }
}

现在,当我们运行这个例子时,它将首先显示由 Spring 框架提供的登录身份验证表单,然后再尝试访问 Web 应用程序的任何 URL。如果用户使用USER角色登录并尝试访问管理员面板,他们将被限制访问。如果用户使用ADMIN角色登录,他们将能够访问用户面板和管理员面板。

身份验证缓存

当应用程序受到最大数量的调用时,Spring Security 的性能成为一个主要关注点。默认情况下,Spring Security 为每个新请求创建一个新会话,并每次准备一个新的安全上下文。在维护用户身份验证时,这会成为一个负担,从而降低性能。

例如,我们有一个 API,每个请求都需要身份验证。如果对该 API 进行多次调用,将会影响使用该 API 的应用程序的性能。因此,让我们在没有缓存实现的情况下了解这个问题。看一下以下日志,我们使用curl命令调用 API,没有缓存实现:

curl -sL --connect-timeout 1 -i http://localhost:8080/authentication-cache/secure/login -H "Authorization: Basic Y3VzdDAwMTpUZXN0QDEyMw=="

看一下以下日志:

21:53:46.302 RDS DEBUG JdbcTemplate - Executing prepared SQL query
21:53:46.302 RDS DEBUG JdbcTemplate - Executing prepared SQL statement [select username,password,enabled from users where username = ?]
21:53:46.302 RDS DEBUG DataSourceUtils - Fetching JDBC Connection from DataSource
21:53:46.302 RDS DEBUG SimpleDriverDataSource - Creating new JDBC Driver Connection to [jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false]
21:53:46.307 RDS DEBUG DataSourceUtils - Returning JDBC Connection to DataSource
21:53:46.307 RDS DEBUG JdbcTemplate - Executing prepared SQL query
21:53:46.307 RDS DEBUG JdbcTemplate - Executing prepared SQL statement [select username,authority from authorities where username = ?]
21:53:46.307 RDS DEBUG DataSourceUtils - Fetching JDBC Connection from DataSource
21:53:46.307 RDS DEBUG SimpleDriverDataSource - Creating new JDBC Driver Connection to [jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false]
21:53:46.307 RDS DEBUG DataSourceUtils - Returning JDBC Connection to DataSource

每次调用此 API 时,它将使用数据库值对用户名和密码进行身份验证。这会影响应用程序的性能,并且如果用户频繁调用,可能会导致不必要的负载。

克服这个问题的一个体面的解决方案之一是缓存用户身份验证一段特定的时间。我们将使用带有正确配置的AuthenticationProviderUserCache的实现,并将其传递给AuthenticationManagerBuilder。我们将使用EhCache来操作缓存对象。我们可以通过以下步骤来使用这个解决方案:

  1. 实现缓存配置类

  2. AuthenticationProvider提供UserCache

  3. AuthenticationManagerBuilder提供AuthenticationProvider

实现缓存配置类

我们创建了以下类,它将提供UserCache bean,并将其提供给AuthenticationProvider

@Configuration
@EnableCaching
public class SpringMvcCacheConfig {

  @Bean
  public EhCacheFactoryBean ehCacheFactoryBean() {
    EhCacheFactoryBean ehCacheFactory = new EhCacheFactoryBean();
    ehCacheFactory.setCacheManager(cacheManagerFactoryBean()
    .getObject());
    return ehCacheFactory;
  }

  @Bean
  public CacheManager cacheManager() {
    return new         
    EhCacheCacheManager(cacheManagerFactoryBean().getObject());
  }

  @Bean
  public EhCacheManagerFactoryBean cacheManagerFactoryBean() {
    EhCacheManagerFactoryBean cacheManager = new 
    EhCacheManagerFactoryBean();
    return cacheManager;
  }

  @Bean
  public UserCache userCache() {
    EhCacheBasedUserCache userCache = new EhCacheBasedUserCache();
    userCache.setCache(ehCacheFactoryBean().getObject());
    return userCache;
  }
}

在上述类中,@EnableCaching启用了缓存管理。

向 AuthenticationProvider 提供 UserCache

现在,我们将创建的UserCache bean 提供给AuthenticationProvider

@Bean
public AuthenticationProvider authenticationProviderBean() {
     DaoAuthenticationProvider authenticationProvider = new              
     DaoAuthenticationProvider();
     authenticationProvider.setPasswordEncoder(passwordEncoder);
     authenticationProvider.setUserCache(userCache);
     authenticationProvider.
     setUserDetailsService(userDetailsService());
     return authenticationProvider;
}

向 AuthenticationManagerBuilder 提供 AuthenticationProvider

现在,在 Spring Security 配置类中向AuthenticationManagerBuilder提供AuthenticationProvider

@Autowired
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws     
    Exception {

       auth
         .eraseCredentials(false)
         //Providing AuthenticationProvider to 
          AuthenticationManagerBuilder.
         .authenticationProvider(authenticationProviderBean())
         .jdbcAuthentication()
         .dataSource(dataSource); 
    }

现在,让我们调用该 API 并检查身份验证的性能。如果我们调用 API 四次,将生成以下日志:

22:46:55.314 RDS DEBUG EhCacheBasedUserCache - Cache hit: false; username: cust001
22:46:55.447 RDS DEBUG JdbcTemplate - Executing prepared SQL query
22:46:55.447 RDS DEBUG JdbcTemplate - Executing prepared SQL statement [select username,password,enabled from users where username = ?]
22:46:55.447 RDS DEBUG DataSourceUtils - Fetching JDBC Connection from DataSource
22:46:55.447 RDS DEBUG SimpleDriverDataSource - Creating new JDBC Driver Connection to [jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false]
22:46:55.463 RDS DEBUG DataSourceUtils - Returning JDBC Connection to DataSource
22:46:55.463 RDS DEBUG JdbcTemplate - Executing prepared SQL query
22:46:55.463 RDS DEBUG JdbcTemplate - Executing prepared SQL statement [select username,authority from authorities where username = ?]
22:46:55.463 RDS DEBUG DataSourceUtils - Fetching JDBC Connection from DataSource
22:46:55.463 RDS DEBUG SimpleDriverDataSource - Creating new JDBC Driver Connection to [jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false]
22:46:55.479 RDS DEBUG DataSourceUtils - Returning JDBC Connection to DataSource
22:46:55.603 RDS DEBUG EhCacheBasedUserCache - Cache put: cust001
22:47:10.118 RDS DEBUG EhCacheBasedUserCache - Cache hit: true; username: cust001
22:47:12.619 RDS DEBUG EhCacheBasedUserCache - Cache hit: true; username: cust001
22:47:14.851 RDS DEBUG EhCacheBasedUserCache - Cache hit: true; username: cust001

正如您在前面的日志中所看到的,最初,AuthenticationProvider从缓存中搜索UserDetails对象;如果它无法从缓存中获取,AuthenticationProvider将查询数据库以获取UserDetails,并将更新后的对象放入缓存中,以便以后的所有调用都将从缓存中检索UserDetails对象。

如果您更新用户的密码并尝试使用新密码对用户进行身份验证,但与缓存中的值不匹配,则它将从数据库中查询UserDetails

使用 Spring Security 实现快速和无状态的 API 身份验证

Spring Security 还提供了用于保护非浏览器客户端(如移动应用程序或其他应用程序)的无状态 API。我们将学习如何配置 Spring Security 来保护无状态 API。此外,我们将找出在设计安全解决方案和提高用户身份验证性能时需要考虑的重要点。

API 身份验证需要 JSESSIONID cookie

对于 API 客户端使用基于表单的身份验证并不是一个好的做法,因为需要在请求链中提供JSESSIONID cookie。Spring Security 还提供了使用 HTTP 基本身份验证的选项,这是一种较旧的方法,但效果很好。在 HTTP 基本身份验证方法中,用户/密码详细信息需要与请求头一起发送。让我们看一下以下 HTTP 基本身份验证配置的示例:

@Override
protected void configure(HttpSecurity http) throws Exception {
      http
        .authorizeRequests()
        .anyRequest().authenticated()
        .and()
        .httpBasic();
}

在上面的示例中,configure()方法来自WebSecurityConfigurerAdapter抽象类,该类提供了此方法的默认实现。子类应该通过调用super来调用此方法,因为它可能会覆盖它们的配置。这种配置方法有一个缺点;每当我们调用受保护的端点时,它都会创建一个新的会话。让我们使用curl命令来调用端点来检查一下:

C:\>curl -sL --connect-timeout 1 -i http://localhost:8080/fast-api-spring-security/secure/login/ -H "Authorization: Basic Y3VzdDAwMTpDdXN0QDEyMw=="
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=B85E9773E6C1E71CE0EC1AD11D897529; Path=/fast-api-spring-security; HttpOnly
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 19
Date: Tue, 27 Mar 2018 18:07:43 GMT

Welcome to the Bank

我们有一个会话 ID cookie;让我们再次调用它:

C:\>curl -sL --connect-timeout 1 -i http://localhost:8080/fast-api-spring-security/secure/login/ -H "Authorization: Basic Y3VzdDAwMTpDdXN0QDEyMw=="
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=14FEB3708295324482BE1DD600D015CC; Path=/fast-api-spring-security; HttpOnly
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 19
Date: Tue, 27 Mar 2018 18:07:47 GMT

Welcome to the Bank

正如您所看到的,每个响应中都有两个不同的会话 ID。在上面的示例中,为了测试目的,我们发送了带有编码的用户名和密码的Authorization头。当您提供用户名和密码进行身份验证时,您可以从浏览器中获取Basic Y3VzdDAwMTpDdXN0QDEyMw==头值。

API 身份验证不需要 JSESSIONID cookie

由于 API 客户端身份验证不需要会话,我们可以通过以下配置轻松摆脱会话 ID:

@Override
protected void configure(HttpSecurity http) throws Exception {
      http
      .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .authorizeRequests()
        .anyRequest().authenticated()
        .and()
        .httpBasic();
}

正如您所看到的,在前面的配置中,我们使用了SessionCreationPolicy.STATELESS。通过这个选项,在响应头中不会添加会话 cookie。让我们看看在这个改变之后会发生什么:

C:\>curl -sL --connect-timeout 1 -i http://localhost:8080/fast-api-spring-security/secure/login/ -H "Authorization: Basic Y3VzdDAwMTpDdXN0QDEyMw=="
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 19
Date: Tue, 27 Mar 2018 18:24:32 GMT

Welcome to the Bank

在上面的示例中,在响应头中找不到会话 cookie。因此,通过这种方式,我们可以使用 Spring Security 管理 API 的无状态身份验证。

使用 JMX 监控和管理 Tomcat

Java 管理扩展JMX)提供了一种强大的机制来监视和管理 Java 应用程序。它可以在 Tomcat 中启用,以监视线程、CPU 使用率和堆内存,并配置MBeans。Spring 提供了开箱即用的 JMX 支持,我们可以使用它轻松地将我们的 Spring 应用程序集成到 JMX 架构中。

JMX 支持提供以下核心功能:

  • 轻松灵活地支持控制 bean 的管理接口

  • 声明支持通过远程连接器公开 MBean

  • 将 Spring bean 自动注册为 JMX MBean

  • 简化支持代理本地和远程 MBean 资源

JMX 功能有三个级别:

  • 仪器级别:这个级别包含由一个或多个 Java bean 表示的组件和资源,这些组件和资源被称为托管 bean或 MBean。

  • 代理级别:这被称为中间代理,称为MBean 服务器。它从远程管理级别获取请求,并将其传递给适当的 MBean。它还可以接收来自 MBean 的与状态更改相关的通知,并将其转发回远程管理级别。

  • 远程管理级别:这一层由连接器、适配器或客户端程序组成。它向代理级别发送请求,并接收请求的响应。用户可以使用连接器或客户端程序(如 JConsole)连接到 MBean 服务器,使用远程方法调用RMI)或Internet 互操作对象协议IIOP)等协议,并使用适配器。

简而言之,远程管理级别的用户向代理级别发送请求,代理级别在仪器级别找到适当的 MBean,并将响应发送回用户。

连接 JMX 以监视 Tomcat

要在 Tomcat 上配置 JMX,我们需要在 JVM 启动时设置相关的系统属性。我们可以使用以下方法。

我们可以在{tomcat-folder}\bin\中更新catalina.shcatalina.bat文件,添加以下值:

-Dcom.sun.management.jmxremote 
-Dcom.sun.management.jmxremote.port={port to access} 
-Dcom.sun.management.jmxremote.authenticate=false 
-Dcom.sun.management.jmxremote.ssl=false

例如,我们可以在{tomcat-folder}\bin\catalina.bat中添加以下值:

set JAVA_OPTS="-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=8990
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false"

如果您想在 Eclipse 中为 Tomcat 配置 JMX,您需要执行以下操作:

  1. 转到“窗口”|“显示视图”|“服务器”。

  2. 双击 localhost 上的 Tomcat v8.0 服务器,打开 Tomcat 概述配置窗口。

  3. 在“常规信息”下,单击“打开启动配置”。

  4. 选择“编辑启动配置属性”的参数选项卡。

  5. 在 VM 参数中,添加以下属性,然后单击“确定”:

-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=8990
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false

做出这些更改后,我们需要重新启动 Tomcat 服务器。之后,我们需要使用 JConsole 测试连接。打开 JConsole 后,我们需要提供远程进程的主机名和端口号,如下所示:

在上面的截图中,我们已经提供了主机名为localhost,端口号为8990。当您单击“连接”时,将会弹出一个对话框,您需要单击“不安全连接”,然后您将连接到 JConsole。

创建 MBean

要创建 MBean,我们可以使用@Managed注解将任何类转换为 MBean。类BankTransferService将金额从一个账户转移到另一个账户。我们将使用此示例进行进一步理解:

@Component
@ManagedResource(objectName = "com.packt.springhighperformance.ch4.mbeans : name=BankMoneyTransferService", description = "Transfers money from one account to another")
public class BankMoneyTransferService {

  private Map<String, Integer> accountMap = new HashMap<String, 
  Integer>();
   {
    accountMap.put("12345", 20000);
    accountMap.put("54321", 10000);
   };

  @ManagedOperation(description = "Amount transfer")
  @ManagedOperationParameters({
      @ManagedOperationParameter(name = "sourceAccount", description = 
       "Transfer from account"),
      @ManagedOperationParameter(name = "destinationAccount",         
        description = "Transfer to account"),
      @ManagedOperationParameter(name = "transferAmount", 
      description = 
        "Amount to be transfer") })
  public void transfer(String sourceAccount, String     
  destinationAccount, int transferAmount) {
    if (transferAmount == 0) {
      throw new IllegalArgumentException("Invalid amount");
    }
    int sourceAcctBalance = accountMap.get(sourceAccount);
    int destinationAcctBalance = accountMap.get(destinationAccount);

    if ((sourceAcctBalance - transferAmount) < 0) {
      throw new IllegalArgumentException("Not enough balance.");
    }
    sourceAcctBalance = sourceAcctBalance - transferAmount;
    destinationAcctBalance = destinationAcctBalance + transferAmount;

    accountMap.put(sourceAccount, sourceAcctBalance);
    accountMap.put(destinationAccount, destinationAcctBalance);
  }

  @ManagedOperation(description = "Check Balance")
  public int checkBalance(String accountNumber) {
    if (StringUtils.isEmpty(accountNumber)) {
      throw new IllegalArgumentException("Enter account no.");
    }
    if (!accountMap.containsKey(accountNumber)) {
      throw new IllegalArgumentException("Account not found.");
    }
    return accountMap.get(accountNumber);
  }

}

在上述类中,@ManagedResource注解将标记类为 MBean,@ManagedAttribute@ManagedOperation注解可用于公开任何属性或方法。@Component注解将确保所有带有@Component@Service@Repository注解的类将被添加到 Spring 上下文中。

在 Spring 上下文中导出 MBean

现在,我们需要在 Spring 应用程序上下文中创建一个MBeanExporter。我们只需要在 Spring 上下文 XML 配置中添加以下标签:

<context:mbean-export/>

我们需要在“‹context:mbean-export/›”元素之前添加component-scan元素;否则,JMX 服务器将无法找到任何 bean。

因此,我们的 Spring 上下文配置将如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans><!-- Skipped schema definitions -->

  <context:component-scan base-
   package="com.packt.springhighperformance.ch4.mbeans" /> 

<context:mbean-export/>

</beans>

现在,我们只需要启动 Tomcat 服务器并打开 JConsole 来查看我们的 MBean。连接到 JConsole 后,转到“MBeans”选项卡,在那里您可以看到我们的包文件夹,其中包含我们的BankMoneyTransferService MBean,列在侧边栏中:

如您在前面的示例中所见,我们的 MBean 已生成并列在 JConsole 中。现在,我们可以通过单击“转账”按钮,调用我们在 MBean 中创建的transfer()方法,从一个账户向另一个账户转账。当我们单击“查看余额”按钮时,它将根据输入的账号号码在弹出窗口中显示当前余额。在后台,它将调用BankMoneyTransferService类的checkBalance()方法。

Spring MVC 性能改进

Spring MVC 应用程序的性能可以通过多种策略和技巧进行改进。在这里,我们列出了一些可以极大改善性能的策略:

  • 使用连接池实现高性能

  • Hibernate 改进

  • 测试改进

  • 适当的服务器维护

  • 使用 Spring Security 的身份验证缓存

  • 实现 Executor 服务框架

使用连接池实现高性能

在 Spring MVC 中提高性能的最重要特性之一是连接池。在这种机制中,创建和管理了N个数据库连接池,以提高应用程序的性能。当应用程序需要使用连接时,它只需请求一个连接,使用它,然后将其返回到池中。这个过程的主要优点是连接池中有连接立即可用,因此可以立即使用。池本身处理连接的生命周期,因此开发人员不必等待连接建立。

Hibernate 改进

另一个提高性能的主要点是关于 Hibernate。脏检查是 Hibernate 提供的一个功能。在脏检查中,Hibernate 会自动识别对象是否被修改并需要更新。Hibernate 会在需要时进行脏检查,以保持性能成本。当特定实体具有对应的具有大量列的表时,成本会增加。为了最小化脏检查成本,我们可以将事务设置为readOnly,这将提高性能并消除任何脏检查的需要。

@Transactional(readOnly=true)
public void performanceTestMethod() {
    ....
}

另一个与 Hibernate 相关的改进是定期刷新和清理 Hibernate 会话。当数据被插入/修改到数据库时,Hibernate 会在会话中存储已经持久化的实体的一个版本,以防它们在会话关闭之前再次更新。我们可以限制 Hibernate 在会话中存储实体的时间,一旦数据被插入,我们就不需要再将实体存储在持久状态中。因此,我们可以安全地刷新和清理entityManager,以使实体的状态与数据库同步,并从缓存中删除实体。这将使应用程序远离内存限制,并肯定会对性能产生积极影响。

entityManager.flush();
entityManager.clear();

另一个改进可以通过使用延迟初始化来实现。如果我们使用 Hibernate,我们应该确保延迟初始化功能被正确使用。我们应该只在需要时才使用实体的延迟加载。例如,如果我们有一个自定义实体集合,如Set<Employee>,配置为延迟初始化,那么该集合的每个实体将使用单独的查询分别加载。因此,如果在集合中延迟初始化了多个实体,那么将会按顺序执行大量查询,这可能会严重影响性能。

测试改进

对于测试改进,我们可以构建一个测试环境,可以在其中执行应用程序,并在其中获取结果。我们可以编写可重复的性能测试脚本,关注绝对性能(如页面渲染时间)和规模上的性能(如负载时的性能下降)。我们可以在测试环境中使用分析器。

适当的服务器维护

一个与适当的服务器维护相关的主要性能方面(如果性能是主要关注点)。以下是一些应该考虑的重要点,以改善性能:

  • 通过创建定期的自动化脚本来清理临时文件。

  • 当多个服务器实例正在运行时使用负载均衡器。

  • 根据应用程序的需求优化配置。例如,在 Tomcat 的情况下,我们可以参考 Tomcat 的配置建议。

使用 Spring Security 的身份验证缓存

在使用 Spring Security 时,可以找到提高性能的重要观点。当请求处理时间被认为是不理想的时候,应该正确配置 Spring Security 以提高性能。可能存在这样一种情况,实际请求处理时间大约为 100 毫秒,而 Spring Security 认证额外增加了 400-500 毫秒。我们可以使用 Spring Security 的认证缓存来消除这种性能成本。

实施 Executor 服务框架

通过所有可能的改进,如果在请求处理方面保持并发性,性能可以得到改善。可能存在这样一种情况,即对我们的应用程序进行多个并发访问的负载测试,这可能会影响我们应用程序的性能。在这种情况下,我们应该调整 Tomcat 服务器上的线程默认值。如果存在高并发性,HTTP 请求将被暂停,直到有一个线程可用来处理它们。

通过在业务逻辑中使用 Executor 框架来扩展默认的服务器线程实现,可以实现并发异步调用。

总结

在本章中,我们对 Spring MVC 模块有了清晰的了解,并学习了不同的配置方法。我们还学习了 Spring 异步处理概念,以及CompletableFeature的实现。之后,我们学习了 Spring Security 模块的配置。我们还了解了 Spring Security 的认证部分和无状态 API。然后,我们学习了 Tomcat 的监控部分和 JMX。最后,我们看了 Spring MVC 的性能改进。

在下一章中,我们将学习关于 Spring 数据库交互的知识。我们将从 Spring JDBC 配置和最佳数据库设计和配置开始。然后,我们将介绍最佳连接池配置。我们还将涵盖@Transactional概念以提高性能。最后,我们将介绍数据库设计的最佳实践。

第五章:理解 Spring 数据库交互

在之前的章节中,我们学习了 Spring 核心特性,如依赖注入(DI)及其配置。我们还看到了如何利用 Spring 面向切面编程AOP)实现可重用的代码。我们学习了如何利用 Spring 模型-视图-控制器MVC)开发松耦合的 Web 应用程序,以及如何通过异步特性、多线程和认证缓存来优化 Spring MVC 实现以获得更好的结果。

在本章中,我们将学习 Spring 框架与数据库交互。数据库交互是应用程序性能中最大的瓶颈。Spring 框架支持所有主要的数据访问技术,如Java 数据库连接JDBC)直接,任何对象关系映射ORM)框架(如 Hibernate),Java 持久化 APIJPA)等。我们可以选择任何数据访问技术来持久化我们的应用程序数据。在这里,我们将探讨 Spring JDBC 的数据库交互。我们还将学习 Spring JDBC 的常见性能陷阱和最佳的数据库设计实践。然后我们将看一下 Spring 事务管理和最佳的连接池配置。

本章将涵盖以下主题:

  • Spring JDBC 配置

  • 为了获得最佳性能的数据库设计

  • 事务管理

  • 使用@Transactional进行声明性 ACID

  • 最佳的隔离级别

  • 最佳的获取大小

  • 最佳的连接池配置

  • Tomcat JDBC 连接池与 HikariCP

  • 数据库设计最佳实践

Spring JDBC 配置

如果不使用 JDBC,我们无法仅使用 Java 连接到数据库。JDBC 将以直接或间接的方式涉及到连接数据库。但是,如果 Java 程序员直接使用核心 JDBC,会面临一些问题。让我们看看这些问题是什么。

核心 JDBC 的问题

当我们使用核心 JDBC API 时,我们将面临以下问题:

    String query = "SELECT COUNT(*) FROM ACCOUNT";

    try (Connection conn = dataSource.getConnection();
        Statement statement = conn.createStatement(); 
        ResultSet rsltSet = statement.executeQuery(query)) 
        {
        if(rsltSet.next()){ 
 int count = rsltSet.getInt(1); System.out.println("count : " + count);
        }
      } catch (SQLException e) {
        // TODO Auto-generated catch block
            e.printStackTrace();
      }      
  }

在前面的例子中,我已经突出显示了一些代码。只有粗体格式的代码是重要的;其余是冗余的代码。因此,我们必须每次都写这些冗余的代码来执行数据库操作。

让我们看看核心 JDBC 的其他问题:

  • JDBC API 异常是被检查的,这迫使开发人员处理错误,增加了应用程序的代码和复杂性

  • 在 JDBC 中,我们必须关闭数据库连接;如果开发人员忘记关闭连接,那么我们的应用程序就会出现一些连接问题

使用 Spring JDBC 解决问题

为了克服核心 JDBC 的前述问题,Spring 框架提供了与 Spring JDBC 模块的出色数据库集成。Spring JDBC 提供了JdbcTemplate类,它帮助我们去除冗余代码,并且帮助开发人员只专注于 SQL 查询和参数。我们只需要配置JdbcTemplatedataSource,并编写如下代码:

jdbcTemplate = new JdbcTemplate(dataSource);
int count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM CUSTOMER", Integer.class);

正如我们在前面的例子中看到的,Spring 通过使用 JDBC 模板简化了处理数据库访问的过程。JDBC 模板在内部使用核心 JDBC 代码,并提供了一种新的高效的处理数据库的方式。与核心 JDBC 相比,Spring JDBC 模板具有以下优势:

  • JDBC 模板通过释放数据库连接自动清理资源

  • 它将核心 JDBC 的SQLException转换为RuntimeExceptions,从而提供更好的错误检测机制

  • JDBC 模板提供了各种方法来直接编写 SQL 查询,因此节省了大量的工作和时间

以下图表显示了 Spring JDBC 模板的高级概述:

Spring JDBC 提供的用于访问数据库的各种方法如下:

  • JdbcTemplate

  • NamedParameterJdbcTemplate

  • SimpleJdbcTemplate

  • SimpleJdbcInsert

  • SimpleJdbcCall

Spring JDBC 依赖项

Spring JDBC 依赖项在pom.xml文件中可用如下:

  • 以下代码是 Spring JDBC 的依赖项:
 <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-jdbc</artifactId>
   <version>${spring.framework.version}</version>
 </dependency>
  • 以下代码是 PostgreSQL 依赖项:
 <dependency>
   <groupId>org.postgresql</groupId>
   <artifactId>postgresql</artifactId>
   <version>42.2.1</version>
 </dependency>

在上面的代码中,我们分别指定了 Spring JDBC 和 PostgreSQL 的依赖项。其余的依赖项将由 Maven 自动解析。在这里,我正在使用 PostgreSQL 数据库进行测试,所以我添加了一个 PostgreSQL 依赖项。如果您使用其他 RDBMS,则应相应地更改依赖项。

Spring JDBC 示例

在这个例子中,我们使用的是 PostgreSQL 数据库。表结构如下:

CREATE TABLE account
(
  accountNumber numeric(10,0) NOT NULL, 
  accountName character varying(60) NOT NULL,
  CONSTRAINT accountNumber_key PRIMARY KEY (accountNumber)
)
WITH (
  OIDS=FALSE
);

我们将使用 DAO 模式进行 JDBC 操作,因此让我们创建一个 Java bean 来模拟我们的Account表:

package com.packt.springhighperformance.ch5.bankingapp.model;

public class Account {
  private String accountName;
  private Integer accountNumber;

  public String getAccountName() {
    return accountName;
  }

  public void setAccountName(String accountName) {
    this.accountName = accountName;
  }

  public Integer getAccountNumber() {
    return accountNumber;
  }

  public void setAccountNumber(Integer accountNumber) {
    this.accountNumber = accountNumber;
  }
  @Override
  public String toString(){
    return "{accountNumber="+accountNumber+",accountName
    ="+accountName+"}";
  }
}

以下的AccountDao接口声明了我们要实现的操作:

public interface AccountDao { 
    public void insertAccountWithJdbcTemplate(Account account);
    public Account getAccountdetails();    
}

Spring bean 配置类如下。对于 bean 配置,只需使用@Bean注解注释一个方法。当JavaConfig找到这样的方法时,它将执行该方法并将返回值注册为BeanFactory中的 bean。在这里,我们注册了JdbcTemplatedataSourceAccountDao beans。

@Configuration
public class AppConfig{
  @Bean
  public DataSource dataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    // PostgreSQL database we are using...
    dataSource.setDriverClassName("org.postgresql.Driver");
    dataSource.setUrl("jdbc:postgresql://localhost:5432/TestDB");
    dataSource.setUsername("test");
    dataSource.setPassword("test");
    return dataSource;
  }

  @Bean
  public JdbcTemplate jdbcTemplate() {
    JdbcTemplate jdbcTemplate = new JdbcTemplate();
    jdbcTemplate.setDataSource(dataSource());
    return jdbcTemplate;
  }

  @Bean
  public AccountDao accountDao() {
    AccountDaoImpl accountDao = new AccountDaoImpl();
    accountDao.setJdbcTemplate(jdbcTemplate());
    return accountDao;
  }

}

在上一个配置文件中,我们创建了DriverManagerDataSource类的DataSource对象。这个类提供了一个我们可以使用的DataSource的基本实现。我们还将 PostgreSQL 数据库的 URL、用户名和密码作为属性传递给dataSource bean。此外,dataSource bean 设置为AccountDaoImpl bean,我们的 Spring JDBC 实现已经准备就绪。该实现是松散耦合的,如果我们想要切换到其他实现或移动到另一个数据库服务器,那么我们只需要在 bean 配置中进行更改。这是 Spring JDBC 框架提供的主要优势之一。

这是AccountDAO的实现,我们在这里使用 Spring 的JdbcTemplate类将数据插入表中:

@Repository
public class AccountDaoImpl implements AccountDao {
  private static final Logger LOGGER = 
  Logger.getLogger(AccountDaoImpl.class);

  private JdbcTemplate jdbcTemplate;

  public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
  }

  @Override
  public void insertAccountWithJdbcTemplate(Account account) {
    String query = "INSERT INTO ACCOUNT (accountNumber,accountName) 
    VALUES (?,?)";

    Object[] inputs = new Object[] { account.getAccountNumber(), 
    account.getAccountName() };
    jdbcTemplate.update(query, inputs);
    LOGGER.info("Inserted into Account Table Successfully");
  }

  @Override
  public Account getAccountdetails() {
    String query = "SELECT accountNumber, accountName FROM ACCOUNT 
    ";
    Account account = jdbcTemplate.queryForObject(query, new 
    RowMapper<Account>(){
      public Account mapRow(ResultSet rs, int rowNum)
          throws SQLException {
            Account account = new Account();
            account.setAccountNumber(rs.getInt("accountNumber"));
            account.setAccountName(rs.getString("accountName")); 
            return account;
      }});
    LOGGER.info("Account Details : "+account);
    return account; 
  }
}

在上一个例子中,我们使用了org.springframework.jdbc.core.JdbcTemplate类来访问持久性资源。Spring 的JdbcTemplate是 Spring JDBC 核心包中的中心类,提供了许多方法来执行查询并自动解析ResultSet以获取对象或对象列表。

以下是上述实现的测试类:

public class MainApp {

  public static void main(String[] args) throws SQLException {
    AnnotationConfigApplicationContext applicationContext = new                             
    AnnotationConfigApplicationContext(
    AppConfig.class);
    AccountDao accountDao = 
    applicationContext.getBean(AccountDao.class);
    Account account = new Account();
    account.setAccountNumber(101);
    account.setAccountName("abc");
    accountDao.insertAccountWithJdbcTemplate(account);
    accountDao.getAccountdetails();
    applicationContext.close();
  }
}

当我们运行上一个程序时,我们会得到以下输出:

May 15, 2018 7:34:33 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@6d5380c2: startup date [Tue May 15 19:34:33 IST 2018]; root of context hierarchy
May 15, 2018 7:34:33 PM org.springframework.jdbc.datasource.DriverManagerDataSource setDriverClassName
INFO: Loaded JDBC driver: org.postgresql.Driver
2018-05-15 19:34:34 INFO AccountDaoImpl:36 - Inserted into Account Table Successfully
2018-05-15 19:34:34 INFO AccountDaoImpl:52 - Account Details : {accountNumber=101,accountName=abc}
May 15, 2018 7:34:34 PM org.springframework.context.support.AbstractApplicationContext doClose
INFO: Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@6d5380c2: startup date [Tue May 15 19:34:33 IST 2018]; root of context hierarchy

为了实现最佳性能的数据库设计

现在,使用现代工具和流程设计数据库非常容易,但我们必须知道这是我们应用程序的一个非常关键的部分,它直接影响应用程序的性能。一旦应用程序实施了不准确的数据库设计,要修复它就太晚了。我们别无选择,只能购买昂贵的硬件来应对问题。因此,我们应该了解一些数据库表设计、数据库分区和良好索引的基本概念和最佳实践,这些可以提高我们应用程序的性能。让我们看看开发高性能数据库应用程序的基本规则和最佳实践。

表设计

表设计类型可以是规范化的或非规范化的,但每种类型都有其自身的好处。如果表设计是规范化的,意味着冗余数据被消除,数据以主键/外键关系逻辑存储,从而提高了数据完整性。如果表设计是非规范化的,意味着增加了数据冗余,并创建了表之间不一致的依赖关系。在非规范化类型中,查询的所有数据通常存储在表中的单行中;这就是为什么检索数据更快,提高了查询性能。在规范化类型中,我们必须在查询中使用连接来从数据库中获取数据,并且由于连接的存在,查询的性能受到影响。我们是否应该使用规范化或非规范化完全取决于我们应用的性质和业务需求。通常,为在线事务处理(OLTP)计划的数据库通常比为在线分析处理(OLAP)计划的数据库更规范化。从性能的角度来看,规范化通常用于需要更多的 INSERT/UPDATE/DELETE 操作的地方,而非规范化用于需要更多 READ 操作的地方。

表的垂直分区

在使用垂直分区时,我们将具有许多列的表分割为具有特定列的多个表。例如,我们不应该在很少查询的表中定义非常宽的文本或二进制大对象(BLOB)数据列,因为性能问题。这些数据必须放置在单独的表结构中,并且可以在查询的表中使用指针。

接下来是一个简单的示例,说明我们如何在 customer 表上使用垂直分区,并将二进制数据类型列 customer_Image 移入单独的表中:

CREATE TABLE customer
(
  customer_ID numeric(10,0) NOT NULL, 
  accountName character varying(60) NOT NULL,
  accountNumber numeric(10,0) NOT NULL,
  customer_Image bytea
);

垂直分区数据如下:

CREATE TABLE customer
(
  customer_Id numeric(10,0) NOT NULL, 
  accountName character varying(60) NOT NULL,
  accountNumber numeric(10,0) NOT NULL
);

CREATE TABLE customer_Image
(
  customer_Image_ID numeric(10,0) NOT NULL, 
  customer_Id numeric(10,0) NOT NULL, 
  customer_Image bytea
);

在 JPA/Hibernate 中,我们可以很容易地将前面的示例映射为表之间的延迟一对多关系。customer_Image 表的数据使用不频繁,因此我们可以将其设置为延迟加载。当客户端请求关系的特定列时,其数据将被检索。

使用索引

我们应该为大表上频繁使用的查询使用索引,因为索引功能是改善数据库模式读性能的最佳方式之一。索引条目以排序顺序存储,这有助于处理 GROUP BY 和 ORDER BY 子句。没有索引,数据库在查询执行时必须执行排序操作。通过索引,我们可以最小化查询执行时间并提高查询性能,但在创建表上的索引时,我们应该注意,也有一些缺点。

我们不应该在频繁更新的表上创建太多索引,因为在表上进行任何数据修改时,索引也会发生变化。我们应该在表上最多使用四到五个索引。如果表是只读的,那么我们可以添加更多索引而不必担心。

以下是为您的应用程序构建最有效索引的指南,对每个数据库都适用:

  • 为了实现索引的最大效益,我们应该在适当的列上使用索引。索引应该用于那些在查询的 WHERE、ORDER BY 或 GROUP BY 子句中频繁使用的列。

  • 始终选择整数数据类型列进行索引,因为它们比其他数据类型列提供更好的性能。保持索引小,因为短索引在 I/O 方面处理更快。

  • 对于检索一系列行的查询,聚集索引通常更好。非聚集索引通常更适合点查询。

使用正确的数据类型

数据类型确定可以存储在数据库表列中的数据类型。当我们创建表时,应根据其存储需求为每个列定义适当的数据类型。例如,SMALLINT占用 2 个字节的空间,而INT占用 4 个字节的空间。当我们定义INT数据类型时,这意味着我们必须每次将所有 4 个字节存储到该列中。如果我们存储像 10 或 20 这样的数字,那么这是字节的浪费。这最终会使您的读取速度变慢,因为数据库必须读取磁盘的多个扇区。此外,选择正确的数据类型有助于我们将正确的数据存储到列中。例如,如果我们为列使用日期数据类型,则数据库不允许在不表示日期的列中插入任何字符串和数字数据。

定义列约束

列约束强制执行对表中可以插入/更新/删除的数据或数据类型的限制。约束的整个目的是在UPDATE/DELETE/INSERT到表中时维护数据完整性。但是,我们应该只在适当的地方定义约束;否则,我们将对性能产生负面影响。例如,定义NOT NULL约束在查询处理过程中不会产生明显的开销,但定义CHECK约束可能会对性能产生负面影响。

使用存储过程

通过使用存储过程在数据库服务器中处理数据来减少网络开销,以及通过在应用程序中缓存数据来减少访问次数,可以调整数据访问性能。

事务管理

数据库事务是任何应用程序的关键部分。数据库事务是一系列被视为单个工作单元的操作。这些操作应该完全完成或根本不产生任何效果。对操作序列的管理称为事务管理。事务管理是任何面向 RDBMS 的企业应用程序的重要部分,以确保数据完整性和一致性。事务的概念可以用四个关键属性来描述:原子性、一致性、隔离性和持久性(ACID)。

事务被描述为 ACID,代表以下内容:

  • 原子性:事务应被视为单个操作单元,这意味着要么整个操作序列完成,要么根本不起作用

  • 一致性:一旦事务完成并提交,那么您的数据和资源将处于符合业务规则的一致状态

  • 隔离:如果同时处理同一数据集的许多事务,则每个事务应该与其他事务隔离开,以防止数据损坏

  • 持久性:一旦事务完成,事务的结果将被写入持久存储,并且由于系统故障无法从数据库中删除

在 Spring 中选择事务管理器

Spring 提供了不同的事务管理器,基于不同的平台。这里,不同的平台意味着不同的持久性框架,如 JDBC、MyBatis、Hibernate 和 Java 事务 API(JTA)。因此,我们必须相应地选择 Spring 提供的事务管理器。

以下图表描述了 Spring 提供的特定于平台的事务管理:

Spring 支持两种类型的事务管理:

  • 程序化:这意味着我们可以直接使用 Java 源代码编写我们的事务。这给了我们极大的灵活性,但很难维护。

  • 声明性:这意味着我们可以通过使用 XML 以集中的方式或者通过使用注释以分布式的方式来管理事务。

使用@Transactional 声明性 ACID

强烈建议使用声明式事务,因为它们将事务管理从业务逻辑中分离出来,并且易于配置。让我们看一个基于注解的声明式事务管理的示例。

让我们使用在 Spring JDBC 部分中使用的相同示例。在我们的示例中,我们使用JdbcTemplate进行数据库交互。因此,我们需要在 Spring 配置文件中添加DataSourceTransactionManager

以下是 Spring bean 配置类:

@Configuration
@EnableTransactionManagement
public class AppConfig {
    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new 
        DriverManagerDataSource(); 
        dataSource.setDriverClassName("org.postgresql.Driver");
        dataSource.setUrl("jdbc:postgresql:
        //localhost:5432/TestDB");
        dataSource.setUsername("test");
        dataSource.setPassword("test");
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate() {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource());
        return jdbcTemplate;
    }

    @Bean
    public AccountDao accountDao(){
      AccountDaoImpl accountDao = new AccountDaoImpl();
      accountDao.setJdbcTemplate(jdbcTemplate());
      return accountDao;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        DataSourceTransactionManager transactionManager = new                                             
        DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource());
        return transactionManager;
    }

}

在之前的代码中,我们创建了一个dataSource bean。它用于创建DataSource对象。在这里,我们需要提供数据库配置属性,比如DriverClassNameUrlUsernamePassword。您可以根据您的本地设置更改这些值。

我们正在使用 JDBC 与数据库交互;这就是为什么我们创建了一个transactionManager类型为org.springframework.jdbc.datasource.DataSourceTransactionManager的 bean。

@EnableTransactionManagement注解用于在我们的 Spring 应用程序中启用事务支持。

以下是一个AccountDao实现类,用于在Account表中创建记录:

@Repository
public class AccountDaoImpl implements AccountDao {
  private static final Logger LOGGER =             
  Logger.getLogger(AccountDaoImpl.class);  
  private JdbcTemplate jdbcTemplate; 

  public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
  }

  @Override
  @Transactional
  public void insertAccountWithJdbcTemplate(Account account) {
    String query = "INSERT INTO ACCOUNT (accountNumber,accountName) 
    VALUES (?,?)";    
    Object[] inputs = new Object[] { account.getAccountNumber(),                                 
    account.getAccountName() };
    jdbcTemplate.update(query, inputs);
    LOGGER.info("Inserted into Account Table Successfully");
    throw new RuntimeException("simulate Error condition");
  }
}

在前面的代码中,我们通过在insertAccountWithJdbcTemplate()方法上注释@Transactional提供了声明式事务管理。@Transactional注解可以用于方法,也可以用于类级别。在前面的代码中,我在插入Account后抛出了RuntimeException异常,以检查在生成异常后事务将如何回滚。

以下是用于检查我们的事务管理实现的main类:

public class MainApp {

  private static final Logger LOGGER = Logger.getLogger(MainApp.class);

  public static void main(String[] args) throws SQLException {
    AnnotationConfigApplicationContext applicationContext = new 
    AnnotationConfigApplicationContext(
    AppConfig.class);

    AccountDao accountDao = 
    applicationContext.getBean(AccountDao.class); 
    Account account = new Account();
    account.setAccountNumber(202);
    account.setAccountName("xyz");
    accountDao.insertAccountWithJdbcTemplate(account); 
    applicationContext.close();
  }
}

现在,当我们运行上面的代码时,我们会得到以下输出:

INFO: Loaded JDBC driver: org.postgresql.Driver
2018-04-09 23:24:09 INFO AccountDaoImpl:36 - Inserted into Account Table Successfully
Exception in thread "main" java.lang.RuntimeException: simulate Error condition at com.packt.springhighperformance.ch5.bankingapp.dao.Impl.AccountDaoImpl.insertAccountWithJdbcTemplate(AccountDaoImpl.java:37)

在前面的日志中,数据成功插入到Account表中。但是,如果您检查Account表,您将找不到一行数据,这意味着在RuntimeException之后事务完全回滚。Spring 框架仅在方法成功返回时才提交事务。如果出现异常,它将回滚整个事务。

最佳隔离级别

正如我们在前一节中学到的,事务的概念是用 ACID 描述的。事务隔离级别是一个概念,不仅适用于 Spring 框架,而且适用于与数据库交互的任何应用程序。隔离级别定义了一个事务对某个数据存储库所做的更改如何影响其他并发事务,以及更改的数据何时以及如何对其他事务可用。在 Spring 框架中,我们与@Transaction注解一起定义事务的隔离级别。

以下片段是一个示例,说明我们如何在事务方法中定义隔离级别:

@Autowired
private AccountDao accountDao;

@Transactional(isolation=Isolation.READ_UNCOMMITTED)
public void someTransactionalMethod(User user) {

  // Interact with accountDao

} 

在上面的代码中,我们定义了一个具有READ_UNCOMMITTED隔离级别的事务方法。这意味着该方法中的事务是以该隔离级别执行的。

让我们在以下部分详细看一下每个隔离级别。

读取未提交

读取未提交是最低的隔离级别。这种隔离级别定义了事务可以读取其他事务仍未提交的数据,这意味着数据与表或查询的其他部分不一致。这种隔离级别确保了最快的性能,因为数据直接从表块中读取,不需要进一步处理、验证或其他验证;但可能会导致一些问题,比如脏读。

让我们看一下以下图表:

在上图中,事务 A写入数据;与此同时,事务 B事务 A提交之前读取了相同的数据。后来,事务 A由于某些异常决定回滚。现在,事务 B中的数据是不一致的。在这里,事务 B运行在READ_UNCOMMITTED隔离级别,因此它能够在提交之前从事务 A中读取数据。

请注意,READ_UNCOMMITTED也可能会产生不可重复读和幻读等问题。当事务隔离级别选择为READ_COMMITTED时,就会出现不可重复读。

让我们详细看看READ_COMMITTED隔离级别。

读已提交

读已提交隔离级别定义了事务不能读取其他事务尚未提交的数据。这意味着脏读不再是一个问题,但可能会出现其他问题。

让我们看看以下的图表:

在这个例子中,事务 A读取了一些数据。然后,事务 B写入了相同的数据并提交。后来,事务 A再次读取相同的数据,可能会得到不同的值,因为事务 B已经对数据进行了更改并提交。这就是不可重复读。

请注意,READ_COMMITTED也可能会产生幻读等问题。幻读发生在选择REPEATABLE_READ作为事务隔离级别时。

让我们详细看看REPEATABLE_READ隔离级别。

可重复读

REPEATABLE_READ隔离级别定义了如果一个事务多次从数据库中读取一条记录,那么所有这些读取操作的结果必须相同。这种隔离有助于防止脏读和不可重复读等问题,但可能会产生另一个问题。

让我们看看以下的图表:

在这个例子中,事务 A读取了一段数据。与此同时,事务 B在相同的范围内插入了新数据,事务 A最初获取并提交了。后来,事务 A再次读取相同的范围,也会得到事务 B刚刚插入的记录。这就是幻读。在这里,事务 A多次从数据库中获取了一系列记录,并得到了不同的结果集。

可串行化

可串行化隔离级别是所有隔离级别中最高和最严格的。它可以防止脏读、不可重复读和幻读。事务在所有级别(读、范围和写锁定)上都会执行锁定,因此它们看起来就像是以串行方式执行的。在可串行化隔离中,我们将确保不会发生问题,但同时执行的事务会被串行执行,从而降低了应用程序的性能。

以下是隔离级别和读现象之间关系的总结:

级别 脏读 不可重复读 幻读
READ_UNCOMMITTED
READ_COMMITTED
REPEATABLE_READ
SERIALIZABLE

如果隔离级别没有被明确设置,那么事务将使用默认的隔离级别,根据相关数据库的设置。

最佳的获取大小

应用程序与数据库服务器之间的网络流量是应用程序性能的关键因素之一。如果我们能减少流量,将有助于提高应用程序的性能。获取大小是一次从数据库中检索的行数。它取决于 JDBC 驱动程序。大多数 JDBC 驱动程序的默认获取大小为 10。在正常的 JDBC 编程中,如果要检索 1000 行,那么您将需要在应用程序和数据库服务器之间进行 100 次网络往返以检索所有行。这将增加网络流量,也会影响性能。但是,如果我们将获取大小设置为 100,那么网络往返的次数将为 10。这将极大地提高您的应用程序性能。

许多框架,如 Spring 或 Hibernate,为您提供非常方便的 API 来执行此操作。如果我们不设置获取大小,那么它将采用默认值并提供较差的性能。

以下是使用标准 JDBC 调用设置FetchSize的方法:

PreparedStatement stmt = null;
ResultSet rs = null;

try 
{
  stmt = conn. prepareStatement("SELECT a, b, c FROM TABLE");
  stmt.setFetchSize(200);

  rs = stmt.executeQuery();
  while (rs.next()) {
    ...
  }
}

在上述代码中,我们可以在每个StatementPreparedStatement上设置获取大小,甚至在ResultSet上设置。默认情况下,ResultSet使用Statement的获取大小;StatementPreparedStatement使用特定 JDBC 驱动程序的获取大小。

我们还可以在 Spring 的JdbcTemplate中设置FetchSize

JdbcTemplate jdbc = new JdbcTemplate(dataSource);
jdbc.setFetchSize(200);

设置获取大小时应考虑以下几点:

  • 确保您的 JDBC 驱动程序支持配置获取大小。

  • 获取大小不应该是硬编码的;保持可配置,因为它取决于 JVM 堆内存大小,在不同环境中会有所不同

  • 如果获取的大小很大,应用程序可能会遇到内存不足的问题

最佳连接池配置

JDBC 在访问数据库时使用连接池。连接池类似于任何其他形式的对象池。连接池通常涉及很少或没有代码修改,但它可以在应用程序性能方面提供显着的好处。数据库连接在创建时执行各种任务,例如在数据库中初始化会话、执行用户身份验证和建立事务上下文。创建连接不是零成本的过程;因此,我们应该以最佳方式创建连接,并减少对性能的影响。连接池允许重用物理连接,并最小化创建和关闭会话的昂贵操作。此外,对于数据库管理系统来说,维护许多空闲连接是昂贵的,连接池可以优化空闲连接的使用或断开不再使用的连接。

为什么连接池有用?以下是一些原因:

  • 频繁打开和关闭连接可能很昂贵;最好进行缓存和重用。

  • 我们可以限制对数据库的连接数。这将阻止在连接可用之前访问连接。这在分布式环境中特别有帮助。

  • 根据我们的需求,我们可以为常见操作使用多个连接池。我们可以为 OLAP 设计一个连接池,为 OLAP 设计另一个连接池,每个连接池都有不同的配置。

在本节中,我们将看到最佳的连接池配置是什么,以帮助提高性能。

以下是用于 PostgreSQL 的简单连接池配置:

<Resource type="javax.sql.DataSource"
            name="jdbc/TestDB"
            factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
            driverClassName="org.postgresql.Driver"
            url="jdbc:postgresql://localhost:5432/TestDB"
            username="test"
            password="test"
/>

调整连接池的大小

我们需要使用以下属性来调整连接池的大小:

  • initialSizeinitialSize属性定义了连接池启动时将建立的连接数。

  • maxActivemaxActive属性可用于限制与数据库建立的最大连接数。

  • maxIdlemaxIdeal属性用于始终保持池中空闲连接的最大数量。

  • minIdleminIdeal属性用于始终保持池中空闲连接的最小数量。

  • timeBetweenEvictionRunsMillis:验证/清理线程每隔timeBetweenEvictionRunsMillis毫秒运行一次。这是一个后台线程,可以测试空闲的废弃连接,并在池处于活动状态时调整池的大小。该线程还负责检测连接泄漏。此值不应设置为低于 1 秒。

  • minEvictableIdleTimeMillis:对象在池中空闲的最短时间。

验证连接

设置此配置的优势是无效的连接永远不会被使用,并且有助于防止客户端错误。此配置的缺点是性能会有一些损失,因为要验证连接,需要向数据库发送一次往返的查询来检查会话是否仍然活动。验证是通过向服务器发送一个小查询来完成的,但此查询的成本可能较低。

用于验证连接的配置参数如下:

  • testOnBorrow:当定义testOnBorrow属性为 true 时,在使用连接对象之前会对其进行验证。如果验证失败,连接对象将被放回池中,然后选择另一个连接对象。在这里,我们需要确保validationQuery属性不为空;否则,配置不会产生任何效果。

  • validationIntervalvalidationInterval属性定义验证连接的频率。它不应超过 34 秒。如果设置一个较大的值,将提高应用程序的性能,但也会增加应用程序中存在陈旧连接的机会。

  • validationQuery:在将连接发送到服务请求之前,使用SELECT 1 PostgreSQL 查询来验证连接池中的连接。

连接泄漏

以下配置设置可以帮助我们检测连接泄漏:

  • removeAbandoned:此标志应为 true。这意味着如果连接超过removeAbandonedTimeout,则会删除废弃的连接。

  • removeAbandonedTimeout:以秒为单位。如果连接运行时间超过removeAbandonedTimeout,则认为连接已被废弃。该值取决于应用程序中运行时间最长的查询。

因此,为了获得最佳的池大小,我们需要修改我们的配置以满足以下条件之一:

<Resource type="javax.sql.DataSource"
            name="jdbc/TestDB"
            factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
            driverClassName="org.postgresql.Driver"
            url="jdbc:postgresql://localhost:5432/TestDB"
            username="test"
            password="test"
            initialSize="10"
            maxActive="100"
            maxIdle="50"
            minIdle="10"
            suspectTimeout="60"
            timeBetweenEvictionRunsMillis="30000"
            minEvictableIdleTimeMillis="60000"
            testOnBorrow="true"
            validationInterval="34000"
            validationQuery="SELECT 1"
            removeAbandoned="true"
            removeAbandonedTimeout="60"
            logAbandoned="true"
/>

Tomcat JDBC 连接池与 HikariCP

有许多开源连接池库可用,如 C3P0、Apache Commons DBCP、BoneCP、Tomcat、Vibur 和 Hikari。但要使用哪一个取决于某些标准。以下标准将帮助决定使用哪个连接池。

可靠性

性能总是很好,但是库的可靠性总是比性能更重要。我们不应该选择一个性能更高但不可靠的库。在选择库时应考虑以下事项:

  • 它被广泛使用吗?

  • 代码是如何维护的?

  • 库中尚未解决的 bug 数量。

  • 开发者和用户的社区。

  • 库的开发活跃程度如何?

性能

性能也被视为重要标准。库的性能取决于其配置方式以及测试环境。我们需要确保我们选择的库在我们自己的环境中,以我们自己的配置具有良好的性能。

功能

查看库提供的功能也很重要。我们应该检查所有参数,还要检查参数的默认值(如果我们没有提供)。此外,我们需要查看一些连接策略,如自动提交、隔离级别和语句缓存。

易用性

使用库时如何轻松配置连接池也很重要。此外,它应该有良好的文档和经常更新。

以下表列出了 Tomcat JDBC 连接池和 HikariCP 之间的区别:

Tomcat JDBC HikariCP
默认情况下不在getConnection()上测试连接。 getConnection()上测试连接。
不关闭被遗弃的打开语句。 跟踪并关闭被遗弃的连接。
默认情况下不重置连接池中连接的自动提交和事务级别;用户必须配置自定义拦截器来执行此操作。 重置自动提交、事务隔离和只读状态。
不使用池准备语句属性。 我们可以使用池准备语句属性。
默认情况下不在连接返回到池中执行rollback() 默认情况下在连接返回到池中执行rollback()

数据库交互最佳实践

本节列出了开发人员在开发任何应用程序时应该注意的一些基本规则。不遵循这些规则将导致性能不佳的应用程序。

使用 Statement 与 PreparedStatement 与 CallableStatement

选择StatementPreparedStatementCallableStatement接口之间的区别;这取决于你计划如何使用接口。Statement接口针对单次执行 SQL 语句进行了优化,而PreparedStatement对象针对将被多次执行的 SQL 语句进行了优化,CallableStatement通常用于执行存储过程:

  • StatementPreparedStatement用于执行普通的 SQL 查询。当特定的 SQL 查询只需执行一次时,它是首选的。该接口的性能非常低。

  • PreparedStatementPreparedStatement接口用于执行参数化或动态 SQL 查询。当特定查询需要多次执行时,它是首选的。该接口的性能优于Statement接口(用于多次执行相同查询时)。

  • CallableStatement:当要执行存储过程时,首选CallableStatement接口。该接口的性能很高。

使用批处理而不是 PreparedStatement

向数据库插入大量数据通常是通过准备一个INSERT语句并多次执行该语句来完成的。这会增加 JDBC 调用的次数并影响性能。为了减少 JDBC 调用的次数并提高性能,可以使用PreparedStatement对象的addBatch方法一次向数据库发送多个查询。

让我们看下面的例子:

PreparedStatement ps = conn.prepareStatement(
"INSERT INTO ACCOUNT VALUES (?, ?)");
for (n = 0; n < 100; n++) {
    ps.setInt(accountNumber[n]);
    ps.setString(accountName[n]);
    ps.executeUpdate();
}

在前面的例子中,PreparedStatement用于多次执行INSERT语句。为了执行前面的INSERT操作,需要 101 次网络往返:一次是为了准备语句,其余 100 次是为了执行INSERT SQL 语句。因此,插入和更新大量数据实际上会增加网络流量,并因此影响性能。

让我们看看如何通过使用“批处理”来减少网络流量并提高性能:

PreparedStatement ps = conn.prepareStatement(
"INSERT INTO ACCOUNT VALUES (?, ?)");
for (n = 0; n < 100; n++) {
    ps.setInt(accountNumber[n]);
    ps.setString(accountName[n]);
    ps.addBatch();
}
ps.executeBatch();

在前面的例子中,我使用了addBatch()方法。它将所有 100 个INSERT SQL 语句合并并仅使用两次网络往返来执行整个操作:一次是为了准备语句,另一次是为了执行合并的 SQL 语句批处理。

最小化数据库元数据方法的使用

尽管几乎没有 JDBC 应用程序可以在没有数据库元数据方法的情况下编写,但与其他 JDBC 方法相比,数据库元数据方法很慢。当我们使用元数据方法时,SELECT语句会使数据库进行两次往返:一次是为了元数据,另一次是为了数据。这是非常耗费性能的。我们可以通过最小化元数据方法的使用来提高性能。

应用程序应该缓存所有元数据,因为它们不会改变,所以不需要多次执行。

有效使用 get 方法

JDBC 提供了不同类型的方法来从结果集中检索数据,如getIntgetStringgetObjectgetObject方法是通用的,可以用于所有数据类型。但是,我们应该始终避免使用getObject,因为它的性能比其他方法差。当我们使用getObject获取数据时,JDBC 驱动程序必须执行额外的处理来确定正在获取的值的类型,并生成适当的映射。我们应该始终使用特定数据类型的方法;这比使用getObject等通用方法提供更好的性能。

我们还可以通过使用列号而不是列名来提高性能;例如,getInt(1)getString(2)getLong(3)。如果我们使用列名而不是列号(例如,getString("accountName")),那么数据库驱动程序首先将列名转换为大写(如果需要),然后将accountName与结果集中的所有列进行比较。这个处理时间直接影响性能。我们应该通过使用列号来减少处理时间。

避免连接池的时机

在某些类型的应用程序上使用连接池肯定会降低性能。如果您的应用程序具有以下任何特征,则不适合连接池:

  • 如果一个应用程序每天重新启动多次,我们应该避免连接池,因为根据连接池的配置,每次启动应用程序时都可能会填充连接,这将导致前期性能损失。

  • 如果您有单用户应用程序,比如只生成报告的应用程序(在这种类型的应用程序中,用户每天只使用应用程序三到四次,用于生成报告),则应避免连接池。与与连接池相关的数据库连接相比,每天建立数据库连接的内存利用率较低。在这种情况下,配置连接池会降低应用程序的整体性能。

  • 如果一个应用程序只运行批处理作业,则使用连接池没有任何优势。通常,批处理作业是在一天、一个月或一年的结束时运行,在性能不那么重要的时候运行。

谨慎选择提交模式

当我们提交事务时,数据库服务器必须将事务所做的更改写入数据库。这涉及昂贵的磁盘输入/输出和驱动程序需要通过套接字发送请求。

在大多数标准 API 中,默认的提交模式是自动提交。在自动提交模式下,数据库对每个 SQL 语句(如INSERTUPDATEDELETESELECT语句)执行提交。数据库驱动程序在每个 SQL 语句操作后向数据库发送提交请求。这个请求需要一个网络往返。即使 SQL 语句的执行对数据库没有做出任何更改,也会发生与数据库的往返。例如,即使执行SELECT语句,驱动程序也会进行网络往返。自动提交模式通常会影响性能,因为需要大量的磁盘输入/输出来提交每个操作。

因此,我们将自动提交模式设置为关闭,以提高应用程序的性能,但保持事务活动也是不可取的。保持事务活动可能会通过长时间持有行锁并阻止其他用户访问行来降低吞吐量。以允许最大并发性的间隔提交事务。

将自动提交模式设置为关闭并进行手动提交对于某些应用程序也是不可取的。例如,考虑一个银行应用程序,允许用户将资金从一个账户转移到另一个账户。为了保护这项工作的数据完整性,需要在更新两个账户的新金额后提交交易。

摘要

在本章中,我们清楚地了解了 Spring JDBC 模块,并学习了 Spring JDBC 如何帮助我们消除在核心 JDBC 中使用的样板代码。我们还学习了如何设计我们的数据库以获得最佳性能。我们看到了 Spring 事务管理的各种好处。我们学习了各种配置技术,如隔离级别、获取大小和连接池,这些技术可以提高我们应用程序的性能。最后,我们看了数据库交互的最佳实践,这可以帮助我们提高应用程序的性能。

在下一章中,我们将看到使用 ORM 框架(如 Hibernate)进行数据库交互,并学习 Spring 中的 Hibernate 配置、常见的 Hibernate 陷阱和 Hibernate 性能调优。

第六章:Hibernate 性能调优和缓存

在上一章中,我们学习了如何使用 JDBC 在我们的应用程序中访问数据库。我们学习了如何优化设计我们的数据库、事务管理和连接池,以获得应用程序的最佳性能。我们还学习了如何通过使用 JDBC 中的准备语句来防止 SQL 注入。我们看到了如何通过使用 JDBC 模板来消除传统的管理事务、异常和提交的样板代码。

在本章中,我们将向一些高级的访问数据库的方式迈进,使用对象关系映射ORM)框架,比如 Hibernate。我们将学习如何通过使用 ORM 以最佳的方式改进数据库访问。通过 Spring Data,我们可以进一步消除实现数据访问对象DAO)接口的样板代码。

本章我们将学习以下主题:

  • Spring Hibernate 和 Spring Data 简介

  • Spring Hibernate 配置

  • 常见的 Hibernate 陷阱

  • Hibernate 性能调优

Spring Hibernate 和 Spring Data 简介

正如我们在之前的章节中看到的,Java 数据库连接JDBC)暴露了一个 API,隐藏了特定于数据库供应商的通信。然而,它存在以下限制:

  • 即使对于琐碎的任务,JDBC 开发也非常冗长

  • JDBC 批处理需要特定的 API,不是透明的

  • JDBC 不提供内置支持显式锁定和乐观并发控制

  • 需要显式处理事务,并且有很多重复的代码

  • 连接查询需要额外的处理来将ResultSet转换为领域模型,或者数据传输对象DTO

几乎所有 JDBC 的限制都被 ORM 框架所覆盖。ORM 框架提供对象映射、延迟加载、急切加载、资源管理、级联、错误处理和其他数据访问层的服务。其中一个 ORM 框架是 Hibernate。Spring Data是由 Spring 框架实现的一层,用于提供样板代码并简化应用程序中使用的不同类型的持久性存储的访问。让我们在接下来的章节中看一下 Spring Hibernate 和 Spring Data 的概述。

Spring Hibernate

Hibernate 起源于 EJB 的复杂性和性能问题。Hibernate 提供了一种抽象 SQL 的方式,并允许开发人员专注于持久化对象。作为 ORM 框架,Hibernate 帮助将对象映射到关系数据库中的表。Hibernate 在引入时有自己的标准,代码与其标准实现紧密耦合。因此,为了使持久性通用化并且与供应商无关,Java 社区进程JCP)制定了一个名为Java 持久化 APIJPA)的标准化 API 规范。所有 ORM 框架都开始遵循这一标准,Hibernate 也是如此。

Spring 并没有实现自己的 ORM;但是,它支持任何 ORM 框架,比如 Hibernate、iBatis、JDO 等。通过 ORM 解决方案,我们可以轻松地将数据持久化并以普通的 Java 对象POJO)的形式从关系数据库中访问。Spring 的 ORM 模块是 Spring JDBC DAO 模块的扩展。Spring 还提供了 ORM 模板,比如我们在第五章中看到的基于 JDBC 的模板,理解 Spring 数据库交互

Spring Data

正如我们所知,在过去几年中,非结构化和非关系型数据库(称为 NoSQL)变得流行。通过 Spring JPA,与关系数据库交流变得容易;那么,我们如何与非关系型数据库交流?Spring 开发了一个名为 Spring Data 的模块,以提供一种通用的方法来与各种数据存储进行交流。

由于每种持久性存储都有不同的连接和检索/更新数据的方式,Spring Data 提供了一种通用的方法来从每个不同的存储中访问数据。

Spring Data 的特点如下:

  • 通过各种存储库轻松集成多个数据存储。Spring Data 为每个数据存储提供了通用接口,以存储库的形式。

  • 根据存储库方法名称提供的约定解析和形成查询的能力。这减少了需要编写的代码量来获取数据。

  • 基本的审计支持,例如由用户创建和更新。

  • 与 Spring 核心模块完全集成。

  • 与 Spring MVC 集成,通过 Spring Data REST 模块公开REpresentational State Transfer (REST)控制器。

以下是 Spring Data 存储库的一个小示例。我们不需要实现此方法来编写查询并按 ID 获取帐户;Spring Data 将在内部完成:

public interface AccountRepository extends CrudRepository<Account, Long> {
   Account findByAccountId(Long accountId);
}

Spring Hibernate 配置

我们知道 Hibernate 是一个持久性框架,它提供了对象和数据库表之间的关系映射,并且具有丰富的功能来提高性能和资源的最佳使用,如缓存、急切和延迟加载、事件监听器等。

Spring 框架提供了完整的支持,以集成许多持久性 ORM 框架,Hibernate 也是如此。在这里,我们将看到 Spring 与 JPA,使用 Hibernate 作为持久性提供程序。此外,我们将看到 Spring Data 与使用 Hibernate 的 JPA 存储库。

使用 Hibernate 的 Spring 与 JPA

正如我们所知,JPA 不是一个实现;它是持久性的规范。Hibernate 框架遵循所有规范,并且还具有其自己的附加功能。在应用程序中使用 JPA 规范使我们可以在需要时轻松切换持久性提供程序。

要单独使用 Hibernate 需要SessionFactory,要使用 Hibernate 与 JPA 需要EntityManager。我们将使用 JPA,以下是基于 Spring 的 Hibernate JPA 配置:

@Configuration
@EnableTransactionManagement
@PropertySource({ "classpath:persistence-hibernate.properties" })
@ComponentScan({ "com.packt.springhighperformance.ch6.bankingapp" })
public class PersistenceJPAConfig {

  @Autowired
  private Environment env;

  @Bean
  public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
    LocalContainerEntityManagerFactoryBean em = new 
    LocalContainerEntityManagerFactoryBean();
    em.setDataSource(dataSource());
    em.setPackagesToScan(new String[] { 
    "com.packt.springhighperformance
    .ch6.bankingapp.model" });

    JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    em.setJpaVendorAdapter(vendorAdapter);
    em.setJpaProperties(additionalProperties());

    return em;
  }

  @Bean
  public BeanPostProcessor persistenceTranslation() {
    return new PersistenceExceptionTranslationPostProcessor();
  }

  @Bean
  public DataSource dataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setDriverClassName(this.env.get
    Property("jdbc.driverClassName"));
    dataSource.setUrl(this.env.getProperty("jdbc.url"));
    dataSource.setUsername(this.env.getProperty("jdbc.user"));
    dataSource.setPassword(this.env.getProperty("jdbc.password"));
    return dataSource;
  }

  @Bean
  public PlatformTransactionManager 
  transactionManager(EntityManagerFactory emf) {
      JpaTransactionManager transactionManager = new         
      JpaTransactionManager();
      transactionManager.setEntityManagerFactory(emf);
      return transactionManager;
  }

  @Bean
  public PersistenceExceptionTranslationPostProcessor 
    exceptionTranslation() {
    return new PersistenceExceptionTranslationPostProcessor();
  }

  private Properties additionalProperties() {
    Properties properties = new Properties();
    properties.setProperty("hibernate.hbm2ddl.auto", 
    this.env.getProperty("hibernate.hbm2ddl.auto"));
    properties.setProperty("hibernate.dialect", 
    this.env.getProperty("hibernate.dialect"));
    properties.setProperty("hibernate.generate_statistics", 
    this.env.getProperty("hibernate.generate_statistics"));
    properties.setProperty("hibernate.show_sql", 
    this.env.getProperty("hibernate.show_sql"));
    properties.setProperty("hibernate.cache.use_second_level_cache", 
    this.env.getProperty("hibernate.cache.use_second_level_cache"));
    properties.setProperty("hibernate.cache.use_query_cache", 
    this.env.getProperty("hibernate.cache.use_query_cache"));
    properties.setProperty("hibernate.cache.region.factory_class", 
    this.env.getProperty("hibernate.cache.region.factory_class"));

    return properties;
  }
}

在前面的配置中,我们使用LocalContainerEntityManagerFactoryBean类配置了EntityManager。我们设置了DataSource来提供数据库的位置信息。由于我们使用的是 JPA,这是一个由不同供应商遵循的规范,我们通过设置HibernateJpaVendorAdapter和设置特定供应商的附加属性来指定我们在应用程序中使用的供应商。

既然我们已经在应用程序中配置了基于 JPA 的 ORM 框架,让我们看看在使用 ORM 时如何在应用程序中创建 DAO。

以下是AbstractJpaDAO类,具有所有 DAO 所需的基本公共方法:

public abstract class AbstractJpaDAO<T extends Serializable> {

    private Class<T> clazz;

    @PersistenceContext
    private EntityManager entityManager;

    public final void setClazz(final Class<T> clazzToSet) {
        this.clazz = clazzToSet;
    }

    public T findOne(final Integer id) {
        return entityManager.find(clazz, id);
    }

    @SuppressWarnings("unchecked")
    public List<T> findAll() {
        return entityManager.createQuery("from " + 
        clazz.getName()).getResultList();
    }

    public void create(final T entity) {
        entityManager.persist(entity);
    }

    public T update(final T entity) {
        return entityManager.merge(entity);
    }

    public void delete(final T entity) {
        entityManager.remove(entity);
    }

    public void deleteById(final Long entityId) {
        final T entity = findOne(entityId);
        delete(entity);
    }
}

以下是AccountDAO类,管理与Account实体相关的方法:

@Repository
public class AccountDAO extends AbstractJpaDAO<Account> implements IAccountDAO {

  public AccountDAO() {
    super();
    setClazz(Account.class);
  }
}

前面的 DAO 实现示例非常基本,这通常是我们在应用程序中做的。如果 DAO 抛出诸如PersistenceException之类的异常,而不是向用户显示异常,我们希望向最终用户显示正确的可读消息。为了在发生异常时提供可读的消息,Spring 提供了一个翻译器,我们需要在我们的配置类中定义如下:

@Bean
  public BeanPostProcessor persistenceTranslation() {
    return new PersistenceExceptionTranslationPostProcessor();
  }

当我们用@Repository注解我们的 DAO 时,BeanPostProcessor命令会起作用。PersistenceExceptionTranslationPostProcessor bean 将作为对使用@Repository注解的 bean 的顾问。请记住,我们在第三章中学习了关于建议的内容,调整面向方面的编程。在受到建议时,它将重新抛出在代码中捕获的 Spring 特定的未检查数据访问异常。

因此,这是使用 Hibernate 的 Spring JPA 的基本配置。现在,让我们看看 Spring Data 配置。

Spring Data 配置

正如我们在介绍中学到的,Spring Data 提供了连接不同数据存储的通用方法。Spring Data 通过 Repository 接口提供基本的抽象。Spring Data 提供的基本存储库如下:

  • CrudRepository 提供基本的 CRUD 操作

  • PagingAndSortingRepository 提供了对记录进行分页和排序的方法

  • JpaRepository 提供了与 JPA 相关的方法,如批量刷新和插入/更新/删除等

在 Spring Data 中,Repository 消除了 DAO 和模板的实现,如 HibernateTemplateJdbcTemplate。Spring Data 是如此抽象,以至于我们甚至不需要为基本的 CRUD 操作编写任何方法实现;我们只需要基于 Repository 定义接口,并为方法定义适当的命名约定。Spring Data 将负责根据方法名创建查询,并将其执行到数据库中。

Spring Data 的 Java 配置与我们在使用 Hibernate 的 Spring JPA 中看到的相同,只是添加了定义存储库。以下是声明存储库到配置的片段:

@Configuration
@EnableTransactionManagement
@PropertySource({ "classpath:persistence-hibernate.properties" })
@ComponentScan({ "com.packt.springhighperformance.ch6.bankingapp" })
 @EnableJpaRepositories(basePackages = "com.packt.springhighperformance.ch6.bankingapp.repository")
public class PersistenceJPAConfig {

}

在本章中,我们不会深入探讨 Hibernate 和 Spring Data 特定的开发。但是,我们将深入探讨在我们的应用程序中不适当使用 Hibernate 或 JPA 以及正确配置时所面临的问题,并提供解决问题的解决方案,以及实现高性能的最佳实践。让我们看看在我们的应用程序中使用 Hibernate 时常见的问题。

常见的 Hibernate 陷阱

JPA 和 Hibernate ORM 是大多数 Java 应用中使用的最流行的框架,用于与关系数据库交互。它们的流行度增加是因为它们使用面向对象域和底层关系数据库之间的映射来抽象数据库交互,并且非常容易实现简单的 CRUD 操作。

在这种抽象下,Hibernate 使用了许多优化,并将所有数据库交互隐藏在其 API 后面。通常情况下,我们甚至不知道 Hibernate 何时会执行 SQL 语句。由于这种抽象,很难找到低效和潜在性能问题。让我们看看我们应用中常见的 Hibernate 问题。

Hibernate n + 1 问题

在使用 JPA 和 Hibernate 时,获取类型对应用程序的性能产生了很大影响。我们应该始终获取我们需要满足给定业务需求的数据。为此,我们将关联实体的 FetchType 设置为 LAZY。当我们将这些关联实体的获取类型设置为 LAZY 时,我们在我们的应用程序中实现了嵌套查询,因为我们不知道在 ORM 框架提供的抽象下这些关联是如何获取的。嵌套查询只是两个查询,其中一个是外部或主查询(从表中获取结果),另一个是针对主查询的每一行结果执行的(从其他表中获取相应或相关数据)。

以下示例显示了我们无意中实现了嵌套查询的情况:

Account account = this.em.find(Account.class, accountNumber);
List<Transaction> lAccountTransactions = account.getTransaction();
for(Transaction transaction : lAccountTransactions){
  //.....
}

大多数情况下,开发人员倾向于编写像前面的示例一样的代码,并且不会意识到像 Hibernate 这样的 ORM 框架可能在内部获取数据。在这里,像 Hibernate 这样的 ORM 框架执行一个查询来获取 account,并执行第二个查询来获取该 account 的交易。两个查询是可以接受的,并且不会对性能产生太大影响。这两个查询是针对实体中的一个关联。

假设我们在Account实体中有五个关联:TransactionsUserProfilePayee等等。当我们尝试从Account实体中获取每个关联时,框架会为每个关联执行一个查询,导致 1 + 5 = 6 个查询。六个查询不会有太大影响,对吧?这些查询是针对一个用户的,那么如果我们的应用程序的并发用户数量是 100 呢?那么我们将有 100 * (1 + 5) = 600 个查询。现在,这将对性能产生影响。在获取Account时的这 1 + 5 个查询被称为 Hibernate 中的n + 1问题。在本章的Hibernate 性能调优部分,我们将看到一些避免这个问题的方法。

在视图中打开会话的反模式

我们在前面的部分中看到,为了推迟获取直到需要关联实体时,我们将关联实体的获取类型设置为LAZY。当我们在呈现层尝试访问这些关联实体时(如果它们在我们的业务(服务)层中没有被初始化),Hibernate 会抛出一个异常,称为LazyInitializationException。当服务层方法完成执行时,Hibernate 提交事务并关闭会话。因此,在呈现视图时,活动会话不可用于获取关联实体。

为了避免LazyInitializationException,其中一个解决方案是在视图中保持一个开放的会话。这意味着我们在视图中保持 Hibernate 会话处于打开状态,以便呈现层可以获取所需的关联实体,然后关闭会话。

为了启用这个解决方案,我们需要向我们的应用程序添加一个 web 过滤器。如果我们只使用 Hibernate,我们需要添加filterOpenSessionInViewFilter;如果我们使用 JPA,那么我们需要添加filter OpenEntityManagerInViewFilter。由于在本章中我们使用的是 JPA 与 Hibernate,以下是添加filter的片段:

<filter>
    <filter-name>OpenEntityManagerInViewFilter</filter-name>
    <filter-class>org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter</filter-class>
   ....
</filter>
...
<filter-mapping>
    <filter-name>OpenEntityManagerInViewFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

开放会话在视图OSIV)模式提供的解决方案乍看起来可能不那么糟糕;然而,使用 OSIV 解决方案存在一些问题。让我们来看看 OSIV 解决方案的一些问题:

  1. 服务层在其方法被调用时打开事务,并在方法执行完成时关闭它。之后,就没有显式的打开事务了。从视图层执行的每个额外查询都将在自动提交模式下执行。自动提交模式在安全和数据库方面可能是危险的。由于自动提交模式,数据库需要立即将所有事务日志刷新到磁盘,导致高 I/O 操作。

  2. 这将违反 SOLID 原则中的单一责任,或者关注点分离,因为数据库语句由服务层和呈现层都执行。

  3. 这将导致我们在前面Hibernate n + 1 问题部分看到的 n + 1 问题,尽管 Hibernate 提供了一些解决方案来应对这种情况:@BatchSizeFetchMode.SUBSELECT,但是,这些解决方案将适用于所有的业务需求,不管我们是否想要。

  4. 数据库连接保持到呈现层完成渲染。这会增加整体数据库连接时间并影响事务吞吐量。

  5. 如果在获取会话或在数据库中执行查询时发生异常,它将发生在呈现视图时,因此不可行地向用户呈现一个干净的错误页面。

未知的 Id.generator 异常

大多数情况下,我们希望为我们的表主键使用数据库序列。为了做到这一点,我们知道我们需要在我们的实体上的@GeneratedValue注解中添加generator属性。@GeneratedValue注解允许我们为我们的主键定义一个策略。

以下是我们在实体中添加的代码片段,用于为我们的主键设置数据库序列:

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "accountSequence")
private Integer id;

在这里,我们认为accountSequence是提供给generator的数据库序列名称;然而,当应用程序运行时,它会产生异常。为了解决这个异常,我们使用@SequenceGenerator注解我们的实体,并给出名称为accountSequence,以及 Hibernate 需要使用的数据库序列名称。以下是如何设置@SequenceGenerator注解的示例:

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "accountSequence")
@SequenceGenerator(name = "accountSequence", sequenceName = "account_seq", initialValue = 100000)
private Long accountId;

我们看到了在实现过程中遇到的常见问题。现在,让我们看看如何调优 Hibernate 以实现高性能。

Hibernate 性能调优

在前面的部分中,我们看到了常见的 Hibernate 陷阱或问题。这些问题并不一定意味着 Hibernate 的错误;有时是由于框架的错误使用,有时是 ORM 框架本身的限制。在接下来的部分中,我们将看到如何提高 Hibernate 的性能。

避免 n + 1 问题的方法

我们已经在Hibernate n + 1 问题部分看到了 n + 1 问题。太多的查询会减慢我们应用的整体性能。因此,为了避免懒加载导致的额外查询,让我们看看有哪些可用的选项。

使用 JPQL 进行 Fetch join

通常,我们调用 DAO 的findById方法来获取外部或父实体,然后调用关联的 getter 方法。这样做会导致 n + 1 查询,因为框架会为每个关联执行额外的查询。相反,我们可以使用EntityManagercreateQuery方法编写一个 JPQL 查询。在这个查询中,我们可以使用JOIN FETCH来连接我们想要与外部实体一起获取的关联实体。以下是如何获取JOIN FETCH实体的示例:

Query query = getEntityManager().createQuery("SELECT a FROM Account AS a JOIN FETCH a.transactions WHERE a.accountId=:accountId", Account.class);
query.setParameter("accountId", accountId);
return (Account)query.getSingleResult();

以下是记录表明只执行了一个查询的日志:

2018-03-14 22:19:29 DEBUG ConcurrentStatisticsImpl:394 - HHH000117: HQL: SELECT a FROM Account AS a JOIN FETCH a.transactions WHERE a.accountId=:accountId, time: 72ms, rows: 3
Transactions:::3
2018-03-14 22:19:29 INFO StatisticalLoggingSessionEventListener:258 - Session Metrics {
    26342110 nanoseconds spent acquiring 1 JDBC connections;
    0 nanoseconds spent releasing 0 JDBC connections;
    520204 nanoseconds spent preparing 1 JDBC statements;
    4487788 nanoseconds spent executing 1 JDBC statements;
    0 nanoseconds spent executing 0 JDBC batches;
    0 nanoseconds spent performing 0 L2C puts;
    0 nanoseconds spent performing 0 L2C hits;
    0 nanoseconds spent performing 0 L2C misses;
    13503978 nanoseconds spent executing 1 flushes (flushing a total of 
    4 entities and 1 collections);
    56615 nanoseconds spent executing 1 partial-flushes (flushing a 
    total of 0 entities and 0 collections)
}

JOIN FETCH告诉entityManager在同一个查询中加载所选实体以及关联的实体。

这种方法的优点是 Hibernate 在一个查询中获取所有内容。从性能的角度来看,这个选项很好,因为所有内容都在一个查询中获取,而不是多个查询。这减少了每个单独查询对数据库的往返。

这种方法的缺点是我们需要编写额外的代码来执行查询。如果实体有许多关联,并且我们需要为每个不同的用例获取不同的关联,那么情况就会变得更糟。因此,为了满足每个不同的用例,我们需要编写不同的查询,带有所需的关联。对于每个用例编写太多不同的查询会变得非常混乱,也很难维护。

如果需要不同的连接获取组合的查询数量较少,这个选项将是一个很好的方法。

在 Criteria API 中的连接获取

这种方法和 JPQL 中的JOIN FETCH一样;但是这次我们使用的是 Hibernate 的 Criteria API。以下是如何在 Criteria API 中使用JOIN FETCH的示例:

CriteriaBuilder criteriaBuilder = 
    getEntityManager().getCriteriaBuilder();
    CriteriaQuery<?> query = 
    criteriaBuilder.createQuery(Account.class);
    Root root = query.from(Account.class);
    root.fetch("transactions", JoinType.INNER);
    query.select(root);
    query.where(criteriaBuilder.equal(root.get("accountId"), 
    accountId));

    return (Account)this.getEntityManager().createQuery(query)
   .getSingleResult();

这个选项和 JPQL 一样有优点和缺点。大多数情况下,当我们使用 Criteria API 编写查询时,它是特定于用例的。因此,在这些情况下,这个选项可能不是一个很大的问题,它是减少执行的查询数量的一个很好的方法。

命名实体图

然后命名实体图是 JPA 2.1 中引入的一个新特性。在这种方法中,我们可以定义需要从数据库查询的实体图。我们可以通过使用@NamedEntityGraph注解在我们的实体类上定义实体图。

以下是如何在实体类上使用@NamedEntityGraph定义图的示例:

@Entity
@NamedEntityGraph(name="graph.transactions", attributeNodes= @NamedAttributeNode("transactions"))
public class Account implements Serializable {

  private static final long serialVersionUID = 1232821417960547743L;

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "account_id", updatable = false, nullable = false)
  private Long accountId;
  private String name;

  @OneToMany(mappedBy = "account", fetch=FetchType.LAZY)
  private List<Transaction> transactions = new ArrayList<Transaction>
  ();
.....
}

实体图定义独立于查询,并定义从数据库中获取哪些属性。实体图可以用作加载或提取图。如果使用加载图,则未在实体图定义中指定的所有属性将继续遵循其默认的FetchType.如果使用提取图,则只有实体图定义指定的属性将被视为FetchType.EAGER,而所有其他属性将被视为LAZY。以下是如何将命名实体图用作fetchgraph的示例:

EntityGraph<?> entityGraph = getEntityManager().createEntityGraph("graph.transactions");
Query query = getEntityManager().createQuery("SELECT a FROM Account AS a WHERE a.accountId=:accountId", Account.class);

query.setHint("javax.persistence.fetchgraph", entityGraph);
query.setParameter("accountId", accountId);
return (Account)query.getSingleResult();

我们不打算在本书中详细介绍命名实体图。这是解决 Hibernate 中 n + 1 问题的最佳方法之一。这是JOIN FETCH的改进版本。与JOIN FETCH相比的优势是它将被用于不同的用例。这种方法的唯一缺点是我们必须为我们想要在单个查询中获取的每种关联组合注释命名实体图。因此,如果我们有太多不同的组合要设置,这可能会变得非常混乱。

动态实体图

动态实体图类似于命名实体图,不同之处在于我们可以通过 Java API 动态定义它。以下是使用 Java API 定义实体图的示例:

EntityGraph<?> entityGraph = getEntityManager().createEntityGraph(Account.class);
entityGraph.addSubgraph("transactions");
Map<String, Object> hints = new HashMap<String, Object>();
hints.put("javax.persistence.fetchgraph", entityGraph);

return this.getEntityManager().find(Account.class, accountId, hints);

因此,如果我们有大量特定于用例的实体图,这种方法将优于命名实体图,在这种方法中,为每个用例在我们的实体上添加注释会使代码难以阅读。我们可以将所有特定于用例的实体图保留在我们的业务逻辑中。使用这种方法的缺点是我们需要编写更多的代码,并且为了使代码可重用,我们需要为每个相关的业务逻辑编写更多的方法。

使用 Hibernate 统计信息查找性能问题

大多数情况下,我们在生产系统上面临缓慢的响应,而我们的本地或测试系统运行良好。这些情况大多是由于数据库查询缓慢引起的。在本地实例中,我们不知道我们在生产中有多少请求和数据量。那么,我们如何找出哪个查询导致问题,而不向我们的应用程序代码添加日志?答案是 Hibernate generate_statistics配置。

我们需要将 Hibernate 属性generate_statistics设置为 true,因为默认情况下此属性为 false。此属性会影响整体性能,因为它记录所有数据库活动。因此,只有在要分析缓慢查询时才启用此属性。此属性将生成总结的多行日志,显示在数据库交互上花费了多少总时间。

如果我们想要记录每个查询的执行,我们需要在日志配置中将org.hibernate.stat启用为DEBUG级别;同样,如果我们想要记录 SQL 查询(带时间),我们需要将org.hibernate.SQL启用为DEBUG级别。

以下是打印日志的示例:

Hibernate 生成统计日志

总体统计信息日志显示了使用的 JDBC 连接数、语句、缓存和执行的刷新次数。我们总是需要首先检查语句的数量,以查看是否存在 n + 1 问题。

使用特定于查询的获取

始终建议仅选择我们用例所需的列。如果使用CriteriaQuery,请使用投影选择所需的列。当表具有太多列时,获取整个实体会降低应用程序的性能,因此数据库需要浏览存储页面的每个块来检索它们,而且我们在用例中可能并不需要所有这些列。此外,如果我们使用实体而不是 DTO 类,持久性上下文必须管理实体,并在需要时获取关联/子实体。这会增加额外开销。而不是获取整个实体,只获取所需的列:

SELECT a FROM Account a WHERE a.accountId= 123456;

按如下方式获取特定列:

SELECT a.accountId, a.name FROM Account a WHERE a.accountId = 123456;

使用特定查询获取的更好方法是使用 DTO 投影。我们的实体由持久性上下文管理。因此,如果我们想要更新它,将ResultSet获取到实体会更容易。我们将新值设置给 setter 方法,Hibernate 将负责更新它的 SQL 语句。这种便利性是以性能为代价的,因为 Hibernate 需要对所有受管理的实体进行脏检查,以找出是否需要将任何更改保存到数据库。DTO 是 POJO 类,与我们的实体相同,但不受持久性管理。

我们可以通过使用构造函数表达式在 JPQL 中获取特定列,如下所示:

entityManager.createQuery("SELECT new com.packt.springhighperformance.ch6.bankingapp.dto.AccountDto(a.id, a.name) FROM Account a").getResultList();

同样,我们可以通过使用CriteriaQueryJPAMetamodel来做同样的事情,如下所示:

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery q = cb.createQuery(AccountDTO.class);
Root root = q.from(Account.class);
q.select(cb.construct(AccountDTO.class, root.get(Account_.accountNumber), root.get(Account_.name)));

List authors = em.createQuery(q).getResultList();

缓存及其最佳实践

我们已经看到了 Spring 中缓存是如何工作的,在第三章中,调整面向方面的编程。在这里,我们将看到 Hibernate 中缓存是如何工作的,以及 Hibernate 中有哪些不同类型的缓存。在 Hibernate 中,有三种不同类型的缓存,如下所示:

  • 一级缓存

  • 二级缓存

  • 查询缓存

让我们了解 Hibernate 中每种缓存机制是如何工作的。

一级缓存

在一级缓存中,Hibernate 在会话对象中缓存实体。Hibernate 一级缓存默认启用,我们无法禁用它。但是,Hibernate 提供了方法,通过这些方法我们可以从缓存中删除特定对象,或者完全清除会话对象中的缓存。

由于 Hibernate 在会话对象中进行一级缓存,任何缓存的对象对另一个会话是不可见的。当会话关闭时,缓存被清除。我们不打算详细介绍这种缓存机制,因为它默认可用,没有办法调整或禁用它。有一些方法可以了解这个级别的缓存,如下所示:

  • 使用会话的evict()方法从 Hibernate 一级缓存中删除单个对象

  • 使用会话的clear()方法完全清除缓存

  • 使用会话的contains()方法检查对象是否存在于 Hibernate 缓存中

二级缓存

数据库抽象层(例如 ORM 框架)的一个好处是它们能够透明地缓存数据:

在数据库和应用程序级别进行缓存

对于许多大型企业应用程序来说,应用程序缓存并不是一个选项。通过应用程序缓存,我们可以减少从数据库缓存中获取所需数据的往返次数。应用程序级缓存存储整个对象,这些对象是根据哈希表键检索的。在这里,我们不打算讨论应用程序级缓存;我们将讨论二级缓存。

在 Hibernate 中,与一级缓存不同,二级缓存是SessionFactory范围的;因此,它由同一会话工厂内创建的所有会话共享。当启用二级缓存并查找实体时,以下内容适用:

  1. 如果实例可用,它将首先在一级缓存中进行检查,然后返回。

  2. 如果一级缓存中不存在实例,它将尝试在二级缓存中查找,如果找到,则组装并返回。

  3. 如果在二级缓存中找不到实例,它将前往数据库并获取数据。然后将数据组装并返回。

Hibernate 本身不进行任何缓存。它提供了接口org.hibernate.cache.spi.RegionFactory,缓存提供程序对此接口进行实现。在这里,我们将讨论成熟且最广泛使用的缓存提供程序 Ehcache。为了启用二级缓存,我们需要将以下两行添加到我们的持久性属性中:

hibernate.cache.use_second_level_cache=true
hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory

启用二级缓存后,我们需要定义要缓存的实体;我们需要使用@org.hibernate.annotations.Cache对这些实体进行注释,如下所示:

@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Account implements Serializable {

}

Hibernate 使用单独的缓存区域来存储实体实例的状态。区域名称是完全限定的类名。Hibernate 提供了不同的并发策略,我们可以根据需求使用。以下是不同的并发策略:

  • READ_ONLY:仅用于从不修改的实体;在修改时会抛出异常。用于一些静态参考数据,不会更改。

  • NONSTRICT_READ_WRITE:在影响缓存数据的事务提交时更新缓存。在更新缓存时,有可能从缓存中获取陈旧的数据。此策略适用于可以容忍最终一致性的要求。此策略适用于很少更新的数据。

  • READ_WRITE:为了在更新缓存时避免获取陈旧数据,此策略使用软锁。当缓存的实体被更新时,缓存中的实体被锁定,并在事务提交后释放。所有并发事务将直接从数据库中检索相应的数据。

  • TRANSACTIONAL:事务策略主要用于 JTA 环境中的分布式缓存。

如果未定义过期和驱逐策略,缓存可能会无限增长,并最终消耗所有内存。我们需要设置这些策略,这取决于缓存提供程序。在这里,我们使用 Ehcache,并且以下是在ehcache.xml中定义过期和驱逐策略的方法:

<ehcache>
    <cache 
    name="com.packt.springhighperformance.ch6.bankingapp.model.Account"     
    maxElementsInMemory="1000" timeToIdleSeconds="0"     
    timeToLiveSeconds="10"/>
</ehcache>

我们中的许多人认为缓存存储整个对象。但是,它并不存储整个对象,而是以分解状态存储它们:

  • 主键不存储,因为它是缓存键

  • 瞬态属性不存储

  • 默认情况下不存储集合关联

  • 除关联之外的所有属性值都以其原始形式存储

  • @ToOne关联的外键仅存储 ID

查询缓存

可以通过添加以下 Hibernate 属性来启用查询缓存:

hibernate.cache.use_query_cache=true

启用查询缓存后,我们可以指定要缓存的查询,如下所示:

Query query = entityManager.createQuery("SELECT a FROM Account a WHERE a.accountId=:accountId", Account.class);
query.setParameter("accountId", 7L);
query.setHint(QueryHints.HINT_CACHEABLE, true);
Account account = (Account)query.getSingleResult();

如果我们再次执行已被查询缓存缓存的相同查询,则在DEBUG模式下打印以下日志:

2018-03-17 15:39:07 DEBUG StandardQueryCache:181 - Returning cached query results
2018-03-17 15:39:07 DEBUG SQL:92 - select account0_.account_id as account_1_0_0_, account0_.name as name2_0_0_ from Account account0_ where account0_.account_id=?

批量执行更新和删除

正如我们所知,ORM 框架(如 Hibernate)在更新或删除实体时会执行两个或更多查询。如果我们要更新或删除少量实体,这是可以接受的,但是想象一下我们要更新或删除 100 个实体的情况。Hibernate 将执行 100 个SELECT查询来获取实体,然后执行另外 100 个查询来更新或删除实体。

为了实现任何应用程序的更好性能,需要执行更少的数据库语句。如果我们使用 JPQL 或本地 SQL 执行相同的更新或删除操作,可以在单个语句中完成。Hibernate 作为 ORM 框架提供了许多好处,可以帮助我们专注于业务逻辑,而不是数据库操作。在 Hibernate 可能昂贵的情况下,例如批量更新和删除,我们应该使用本地数据库查询来避免开销并实现更好的性能。

以下是我们可以执行本机查询以将银行收件箱中所有用户的电子邮件更新为“已读”的方法:

entityManager.createNativeQuery("UPDATE mails p SET read = 'Y' WHERE user_id=?").setParameter(0, 123456).executeUpdate();

我们可以通过记录System.currentTimeMillis()来测量使用 Hibernate 方法和本机查询更新大量数据的性能差异。本机查询的性能应该显著提高,比 Hibernate 方法快 10 倍。

本地查询肯定会提高批量操作的性能,但与此同时,它会带来一级缓存的问题,并且不会触发任何实体生命周期事件。众所周知,Hibernate 将我们在会话中使用的所有实体存储在一级缓存中。这对于写后优化很有好处,并且避免在同一会话中为相同的实体执行重复的选择语句。但是,对于本地查询,Hibernate 不知道哪些实体已更新或删除,并相应地更新一级缓存。如果我们在同一会话中在执行本地查询之前获取实体,则它将继续在缓存中使用实体的过时版本。以下是使用本地查询时一级缓存的问题示例:

private void performBulkUpdateIssue(){
    Account account = this.entityManager.find(Account.class, 7L);

    entityManager.createNativeQuery("UPDATE account a SET name = 
    name 
    || '-updated'").executeUpdate();
    _logger.warn("Issue with Account Name: "+account.getName());

    account = this.entityManager.find(Account.class, 7L);
    _logger.warn("Issue with Account Name: "+account.getName());
  }

解决这个问题的方法是在本地查询执行之前手动更新一级缓存,通过在本地查询执行之前分离实体,然后在本地查询执行后重新附加它。为此,请执行以下操作:

private void performBulkUpdateResolution(){
    //make sure you are passing right account id    
    Account account = this.entityManager.find(Account.class, 7L);

 //remove from persistence context
 entityManager.flush();
 entityManager.detach(account);
    entityManager.createNativeQuery("UPDATE account a SET name = 
    name 
    || '-changed'").executeUpdate();
    _logger.warn("Resolution Account Name: "+account.getName());

    account = this.entityManager.find(Account.class, 7L);
    _logger.warn("Resolution Account Name: "+account.getName());
  }

在执行本地查询之前,请调用flush()detach()方法。flush()方法告诉 Hibernate 将一级缓存中的更改实体写入数据库。这是为了确保我们不会丢失任何更新。

Hibernate 编程实践

到目前为止,我们看到了当 Hibernate 没有被最佳利用时出现的问题,以及如何使用 Hibernate 来实现更好的性能。以下是在使用 JPA 和 Hibernate 时遵循的最佳实践(在缓存和一般情况下)以实现更好的性能。

缓存

以下是关于 Hibernate 不同缓存级别的一些编程提示:

  • 确保使用与 Hibernate 版本相同的hibernate-ehcache版本。

  • 由于 Hibernate 将所有对象缓存到会话的一级缓存中,因此在运行批量查询或批量更新时,有必要定期清除缓存以避免内存问题。

  • 在使用二级缓存缓存实体时,默认情况下不会缓存实体内的集合。为了缓存集合,需要在实体内用@Cacheable@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)注释集合。每个集合都存储在二级缓存中的单独区域中,区域名称是实体类的完全限定名称加上集合属性的名称。为每个缓存的集合单独定义过期和驱逐策略。

  • 当使用 JPQL 执行 DML 语句时,Hibernate 将更新/驱逐这些实体的缓存;然而,当使用本地查询时,整个二级缓存将被驱逐,除非在使用 Hibernate 与 JPA 时添加以下细节到本地查询执行中:

Query nativeQuery = entityManager.createNativeQuery("update Account set name='xyz' where name='abc'");

nativeQuery.unwrap(org.hibernate.SQLQuery.class).addSynchronizedEntityClass(Account.class);

nativeQuery.executeUpdate();
  • 在查询缓存的情况下,每个查询和参数值的组合将有一个缓存条目,因此对于预期有不同参数值组合的查询,不适合缓存。

  • 在查询缓存的情况下,对于从数据库中频繁更改的实体类进行抓取的查询不适合缓存,因为当涉及查询的任何实体发生更改时,缓存将被作废。

  • 所有查询缓存结果都存储在org.hibernate.cache.internal.StandardQueryCache区域。我们可以为这个区域指定过期和驱逐策略。此外,如果需要,我们可以使用查询提示org.hibernate.cacheRegion为特定查询设置不同的缓存区域。

  • Hibernate 在名为org.hibernate.cache.spi.UpdateTimestampsCache的区域中保留了所有查询缓存表的最后更新时间戳。Hibernate 使用这个来验证缓存的查询结果是否过时。最好关闭此缓存区域的自动驱逐和过期,因为只要缓存结果区域中有缓存的查询结果,这个缓存中的条目就不应该被驱逐/过期。

杂项

以下是实现应用程序更好性能的一般 Hibernate 最佳实践:

  • 避免在生产系统上启用generate_statistics;而是通过在生产系统的暂存或副本上启用generate_statistics来分析问题。

  • Hibernate 始终更新所有数据库列,即使我们只更新一个或几个列。UPDATE语句中的所有列将比少数列花费更多时间。为了实现高性能并避免在UPDATE语句中使用所有列,只包括实际修改的列,并在实体上使用@DynamicUpdate注释。此注释告诉 Hibernate 为每个更新操作生成一个新的 SQL 语句,仅包含修改的列。

  • 将默认的FetchType设置为LAZY以用于所有关联,并使用特定于查询的获取,使用JOIN FETCH,命名实体图或动态实体图,以避免 n + 1 问题并提高性能。

  • 始终使用绑定参数以避免 SQL 注入并提高性能。与绑定参数一起使用时,如果多次执行相同的查询,Hibernate 和数据库会优化查询。

  • 在大型列表中执行UPDATEDELETE,而不是逐个执行它们。我们已经在在大量中执行更新和删除部分中讨论过这一点。

  • 不要对只读操作使用实体;而是使用 JPA 和 Hibernate 提供的不同投影。我们已经看到的一个是 DTO 投影。对于只读需求,将实体更改为SELECT中的构造函数表达式非常容易,并且将实现高性能。

  • 随着 Java 8.0 中 Stream API 的引入,许多人使用其功能来处理从数据库检索的大量数据。Stream 旨在处理大量数据。但是数据库可以做一些事情比 Stream API 更好。不要对以下要求使用 Stream API:

  • 过滤数据:数据库可以更有效地过滤数据,而我们可以使用WHERE子句来实现

  • 限制数据:当我们想要限制要检索的数据的数量时,数据库提供比 Stream API 更有效的结果

  • 排序数据:数据库可以通过使用ORDER BY子句更有效地进行排序,而不是 Stream API

  • 使用排序而不是排序,特别是对于大量关联数据的实体。排序是 Hibernate 特定的,不是 JPA 规范:

  • Hibernate 使用 Java 比较器在内存中进行排序。但是,可以使用关联实体上的@OrderBy注释从数据库中检索相同所需顺序的数据。

  • 如果未指定列名,则将在主键上执行@OrderBy

  • 可以在@OrderBy中指定多个列,以逗号分隔。

  • 数据库比在 Java 中实现排序更有效地处理@OrderBy。以下是一个代码片段,作为示例:

@OneToMany(mappedBy = "account", fetch=FetchType.LAZY)
@OrderBy("created DESC")
private List<Transaction> transactions = new ArrayList<Transaction>();
  • Hibernate 定期对与当前PersistenceContext关联的所有实体执行脏检查,以检测所需的数据库更新。对于从不更新的实体,例如只读数据库视图或表,执行脏检查是一种开销。使用@Immutable对这些实体进行注释,Hibernate 将在所有脏检查中忽略它们,从而提高性能。

  • 永远不要定义单向的一对多关系;总是定义双向关系。如果定义了单向的一对多关系,Hibernate 将需要一个额外的表来存储两个表的引用,就像在多对多关系中一样。在单向方法的情况下,会执行许多额外的 SQL 语句,这对性能不利。为了获得更好的性能,在实体的拥有方上注释@JoinColumn,并在实体的另一侧使用mappedby属性。这将减少 SQL 语句的数量,提高性能。需要明确处理从关系中添加和删除实体;因此,建议在父实体中编写辅助方法,如下所示:

@Entity
public class Account {

    @Id
    @GeneratedValue
    private Integer id;

 @OneToMany(mappedBy = "account")
    private List<Transaction> transactions = new ArrayList<>();

    public void addTransaction(Transaction transaction) {
 transactions.add(transaction);
 transaction.setPost(this);
 }

 public void removeTransaction(Transaction transaction) {
 transactions.remove(transaction);
 transaction.setPost(null);
 }
}

@Entity
public class Transaction {

    @Id
    @GeneratedValue
    private Integer id;

    @ManyToOne(fetch = FetchType.LAZY)
 @JoinColumn(name = "account_id")
    private Account account;
}

摘要

我们从基本配置 ORM 框架 Hibernate 开始了本章,使用 JPA 和 Spring Data。我们关注了在生产中遇到的常见 ORM 问题。在本章中,我们学习了在使用 Hibernate 进行数据库操作和实现高性能时所面临的常见问题的最佳解决方案。我们学习了在基于 ORM 的框架上工作时要遵循的最佳实践,以在开发阶段就实现高性能,而不是在生产系统中面对问题时解决它们。

与优化和高性能一致,下一章提供了关于 Spring 消息优化的信息。正如您所知,消息框架企业应用程序连接多个客户端,并提供可靠性、异步通信和松散耦合。框架被构建为提供各种好处;然而,如果我们不以最佳方式使用它们,就会面临问题。同样,如果有效使用与队列配置和可伸缩性相关的某些参数,将最大化我们企业应用程序的 Spring 消息框架的吞吐量。

第七章:优化 Spring 消息

在上一章中,我们学习了使用对象关系映射ORM)框架(如 Hibernate)访问数据库的不同高级方法。我们还学习了在使用 ORM 时如何以最佳方式改进数据库访问。我们研究了 Spring Data 来消除实现数据访问对象DAO)接口的样板代码。在本章末尾,我们看到了 Hibernate 的最佳实践。

在本章中,我们将学习 Spring 对消息传递的支持。消息传递是一种非常强大的技术,有助于扩展应用程序,并鼓励我们解耦架构。

Spring 框架提供了广泛的支持,通过简化使用Java 消息服务JMS)API 来将消息系统集成到我们的应用程序中,以异步接收消息。消息解决方案可用于从应用程序中的一个点发送消息到已知点,以及从应用程序中的一个点发送消息到许多其他未知点。这相当于面对面分享和通过扩音器向一群人分享东西。如果我们希望将消息发送到一组未知的客户端,那么我们可以使用队列将消息广播给正在监听的人。

以下是本章将涵盖的主题:

  • 什么是消息传递?

  • AMQP 是什么?

  • 我们为什么需要 AMQP?

  • RabbitMQ

  • Spring 消息配置

什么是消息传递?

消息传递是软件组件或应用程序之间交互的一种模式,其中客户端可以向任何其他客户端发送消息,并从任何其他客户端接收消息。

这种消息交换可以使用一个名为broker的组件来完成。broker 提供了所有必要的支持和服务来交换消息,同时具有与其他接口交互的能力。这些接口被称为消息导向中间件MOM)。以下图表描述了基于 MOM 的消息系统:

使用 AMQP、STOMP 和 XMPP 协议减少开发分布式应用程序的复杂性的消息系统。让我们详细讨论它们:

  • AMQP:AMQP 是一种开放的、标准的异步消息系统应用层协议。在 AMQP 中,消息应以二进制格式传输。

  • STOMPSTOMP代表简单文本导向消息协议。STOMP 提供了一个兼容的介质,允许系统与几乎所有可用的消息代理进行通信。

  • XMPPXMPP代表可扩展消息和出席协议。这是一种基于 XML 的开放标准通信协议,用于消息导向中间件。

什么是 AMQP?

高级消息队列协议AMQP)是一种开放的标准应用层协议。传输的每个字节都是指定的,这使得它可以在许多其他语言和操作系统架构中使用。因此,这使得它成为一个跨平台兼容的协议。AMQP 受到多个消息代理的支持,如 RabbitMQ、ActiveMQ、Qpid 和 Solace。Spring 提供了基于 AMQP 的消息实现解决方案。Spring 提供了一个模板,用于通过消息代理发送和接收消息。

JMS API 的问题

JMS API 用于在 Java 平台上发送和接收消息。Spring 通过在 JMS 层周围提供额外的层来支持简化使用 JMS API 的方法。这一层改进了发送和接收消息的过程,还处理连接对象的创建和释放。

开发人员广泛使用 JMS API 来创建基于 Java 的消息系统。使用 JMS API 的主要缺点是平台矛盾,这意味着我们可以使用 JMS API 来开发与基于 Java 的应用程序兼容的消息系统。JMS API 不支持其他编程语言。

我们为什么需要 AMQP?

AMQP 是解决 JMS API 问题的解决方案。使用 AMQP 的基本优势在于,它支持消息的交换,不受平台兼容性和消息代理的影响。我们可以使用任何编程语言开发消息系统,仍然可以使用基于 AMQP 的消息代理与每个系统进行通信。

AMQP 和 JMS API 之间的区别

以下是 AMQP 和 JMS API 之间的一些重要区别:

  • 平台兼容性

  • 消息模型

  • 消息数据类型

  • 消息结构

  • 消息路由

  • 工作流策略

这些在以下部分中有更详细的解释。

平台兼容性

JMS 应用程序可以与任何操作系统一起工作,但它们仅支持 Java 平台。如果我们想要开发一个可以与多个系统通信的消息系统,那么所有这些系统都应该使用 Java 编程语言开发。

在使用 AMQP 时,我们可以开发一个可以与不同技术的任何系统进行通信的消息系统。因此,不需要目标系统使用相同的技术进行开发。

消息模型

JMS API 提供两种消息模型,即点对点和发布-订阅,用于不同平台系统之间的异步消息传递。

AMQP 支持以下交换类型:直接、主题、扇出和页眉。

消息数据类型

JMS API 支持五种标准消息类型:

  • StreamMessage

  • MapMessage

  • TextMessage

  • ObjectMessage

  • BytesMessage

AMQP 仅支持一种类型的消息——二进制消息;消息必须以二进制格式传输。

消息结构

JMS API 消息具有基本结构,包括头部、属性和正文三个部分。它定义了一个标准形式,应该在所有 JMS 提供程序中可移植。

AMQP 消息包括四个部分:头部、属性、正文和页脚。

消息路由

对于消息路由,AMQP 也可以用于复杂的路由方案,这是通过路由键和基于目标匹配标准实现的。

JMS API 基于更复杂的路由方案,这些方案基于分层主题和客户端消息选择过滤器。

工作流策略

在 AMQP 中,生产者首先需要将消息发送到交换,然后才会转移到队列,而在 JMS 中,不需要交换,因为消息可以直接发送到队列或主题。

交换、队列和绑定是什么?

AMQP 处理发布者和消费者。发布者发送消息,消费者接收消息。消息代理负责这个机制,以确保来自发布者的消息传递到正确的消费者。消息代理使用的两个关键元素是交换和队列。以下图表说明了发布者如何连接到消费者:

让我们了解一下交换、队列和绑定的术语。

交换

交换负责接收消息并将其路由到零个或多个队列。每个代理的交换都有一个唯一的名称,以及虚拟主机中的其他一些属性。所使用的消息路由算法取决于交换类型和绑定。正如我们之前提到的,有四种不同类型的交换:直接、主题、扇出和页眉。

队列

队列是消息消费者接收消息的组件。队列有一个唯一的名称,以便系统可以引用它们。队列名称可以由应用程序定义,也可以在请求时由代理生成。我们不能使用以amq.开头的队列名称,因为它被代理保留用于内部使用。

绑定

绑定用于连接队列和交换机。有一些称为路由键头的标准头部,经纪人使用它们将消息与队列匹配。每个队列都有一个特定的绑定键,如果该键与路由键头的值匹配,队列将接收消息。

介绍 RabbitMQ

RabbitMQ 基于 AMQP,是最广泛使用的轻量级、可靠、可扩展、便携和强大的消息代理之一,使用 Erlang 编写。RabbitMQ 之所以受欢迎的重要原因是它易于设置,并且适合云规模。RabbitMQ 是开源的,并受大多数操作系统和平台支持。使用 RabbitMQ 的应用程序可以通过一个平台中立的、线级协议——AMQP 与其他系统通信。现在,让我们来了解如何配置 RabbitMQ。

设置 RabbitMQ 服务器

在开发消息系统之前,我们需要设置一个消息代理,用于处理发送和接收消息。RabbitMQ 是 AMQP 服务器,可以在www.rabbitmq.com/download.html免费下载。

安装 RabbitMQ 服务器后,根据安装路径,您将不得不使用RABBITMQ_HOME设置以下系统变量:

RABBITMQ_HOME=D:\Apps\RabbitMQ Server\rabbitmq_server-3.6.0

设置好一切后,您可以通过http://localhost:15672/访问 RabbitMQ 控制台。

您将看到默认的登录屏幕,您需要输入guest作为默认用户名和guest作为密码:

登录后,您将看到 RabbitMQ 服务器主页,您可以在那里管理队列、交换和绑定:

现在,我们将通过一个示例来了解 Spring 应用程序中的消息配置。

Spring 消息配置

在开始示例之前,我们需要了解配置消息应用程序的基本设置要求。我们将创建一个 RabbitMQ 消息应用程序,并了解配置的不同部分。在 Spring 应用程序中设置消息涉及以下步骤:

  1. 配置 RabbitMQ 的 Maven 依赖项

  2. 配置 RabbitMQ

  3. 创建一个组件来发送和接收消息

为 RabbitMQ 配置 Maven 依赖项

让我们从向pom.xml添加 RabbitMQ 的依赖开始。以下代码显示了要配置的依赖项:

<dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-rabbit</artifactId>
    <version>${rabbitmq.version}</version>
</dependency>

我们已经为 RabbitMQ 添加了依赖项。现在,让我们创建一个类来配置队列、交换和它们之间的绑定。

配置 RabbitMQ

现在,我们将通过配置部分来清楚地了解ConnectionFactoryRabbitTemplateQueueExchangeBinding、消息监听容器和消息转换器的配置。

配置 ConnectionFactory

对于ConnectionFactory接口,有一个具体的实现CachingConnectionFactory,默认情况下创建一个可以由整个应用程序共享的单个连接代理。用于创建CachingConnectionFactory的代码如下:

@Bean
public ConnectionFactory connectionFactory() {
        CachingConnectionFactory connectionFactory = new 
        CachingConnectionFactory("localhost");
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
        return connectionFactory;
}

我们还可以使用CachingConnectionFactory配置缓存连接,以及仅通道。我们需要将cacheMode属性设置为CacheMode.CONNECTION,使用setCacheMode()。我们还可以通过使用setConnectionLimit()限制允许的连接总数。当设置了此属性并且超过了限制时,channelCheckoutTimeLimit用于等待连接变为空闲。

配置队列

现在,我们将使用Queue类配置一个队列。以下代码创建了一个具有特定名称的队列:

@Bean
public Queue queue() {
    return new Queue(RABBIT_MESSAGE_QUEUE, true);
}

上述的queue()方法使用RABBIT_MESSAGE_QUEUE常量声明了一个具有特定名称的 AMQP 队列。我们还可以使用durable标志设置持久性。我们需要将它作为布尔类型与第二个构造函数参数一起传递。

配置交换

现在,我们需要创建一个 AMQP 交换,消息生产者将向其发送消息。Exchange接口表示一个 AMQP 交换。Exchange接口类型有四种实现:DirectExchangeTopicExchangeFanoutExchangeHeadersExchange。根据我们的需求,我们可以使用任何交换类型。我们将使用以下代码使用DirectExchange

@Bean
public DirectExchange exchange() {
    return new DirectExchange(RABBIT_MESSAGE_EXCHANGE);
}

exchange()方法使用在RABBIT_MESSAGE_EXCHANGE下定义的特定名称创建DirectExchange。我们还可以使用持久性标志设置持久性。我们需要将它作为布尔类型与第二个构造函数参数一起传递。

配置绑定

现在,我们需要使用BindingBuilder类创建一个绑定,将queue连接到Exchange。以下代码用于创建绑定:

@Bean
Binding exchangeBinding(DirectExchange directExchange, Queue queue) {
    return BindingBuilder.bind(queue).
        to(directExchange)
        .with(ROUTING_KEY);
}

exchangeBinding()方法使用ROUTING_KEY路由键值创建queueExchange的绑定。

配置 RabbitAdmin

RabbitAdmin用于声明在启动时需要准备好的交换、队列和绑定。RabbitAdmin自动声明队列、交换和绑定。这种自动声明的主要好处是,如果由于某种原因连接断开,它们将在重新建立连接时自动应用。以下代码配置了RabbitAdmin

@Bean
public RabbitAdmin rabbitAdmin() {
    RabbitAdmin admin = new RabbitAdmin(connectionFactory());
    admin.declareQueue(queue());
    admin.declareExchange(exchange());
    admin.declareBinding(exchangeBinding(exchange(), queue()));
    return admin;
}

rabbitAdmin()将声明QueueExchangeBindingRabbitAdmin构造函数使用connectionFactory() bean 创建一个实例,它不能为null

RabbitAdmin仅在CachingConnectionFactory缓存模式为CHANNEL(默认情况下)时执行自动声明。这种限制的原因是因为可能会将独占和自动删除队列绑定到连接。

配置消息转换器

在监听器接收到消息的确切时间,会发生两个变化步骤。在初始步骤中,传入的 AMQP 消息会使用MessageConverter转换为 Spring 消息Message。在第二步中,当执行目标方法时,如果需要,消息的有效负载会转换为参数类型。默认情况下,在初始步骤中,使用MessageConverter作为 Spring AMQP 的SimpleMessageConverter,它处理转换为 String 和java.io.Serializable

在第二步中,默认情况下使用GenericMessageConverter进行转换。我们在以下代码中使用了Jackson2JsonMessageConverter

@Bean
public MessageConverter messageConverter() {
    return new Jackson2JsonMessageConverter();
}

在下一节中,我们将使用这个消息转换器作为属性来更改默认的消息转换器,同时配置RabbitTemplate

创建一个 RabbitTemplate

Spring AMQP 的RabbitTemplate提供了基本的 AMQP 操作。以下代码使用connectionFactory创建了RabbitTemplate的实例:

@Bean
public RabbitTemplate rabbitTemplate() {
    RabbitTemplate template = new RabbitTemplate(connectionFactory());
    template.setRoutingKey(ROUTING_KEY);
    template.setExchange(RABBIT_MESSAGE_EXCHANGE);
    template.setMessageConverter(messageConverter());
    return template;
}

RabbitTemplate充当生产者发送消息和消费者接收消息的辅助类。

配置监听器容器

要异步接收消息,最简单的方法是使用注释的监听器端点。我们将使用@RabbitListener注释作为消息listener端点。要创建这个listener端点,我们必须使用SimpleRabbitListenerContainerFactory类配置消息listener容器,这是RabbitListenerContainerFactory接口的实现。以下代码用于配置SimpleRabbitListenerContainerFactory

@Bean
public SimpleRabbitListenerContainerFactory listenerContainer() {
    SimpleRabbitListenerContainerFactory factory = new 
    SimpleRabbitListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory());
    factory.setMaxConcurrentConsumers(5);
    return factory;
}

listenerContainer()方法将实例化SimpleRabbitListenerContainerFactory。您可以使用setMaxConcurrentConsumers()方法的maxConcurrentConsumers属性设置最大消费者数量。

以下是包含所有先前讨论的配置方法的类:

@Configuration
@ComponentScan("com.packt.springhighperformance.ch7.bankingapp")
@EnableRabbit
public class RabbitMqConfiguration {

  public static final String RABBIT_MESSAGE_QUEUE = 
  "rabbit.queue.name";
  private static final String RABBIT_MESSAGE_EXCHANGE =     
  "rabbit.exchange.name";
  private static final String ROUTING_KEY = "messages.key";

  @Bean
  public ConnectionFactory connectionFactory() {
    CachingConnectionFactory connectionFactory = new 
    CachingConnectionFactory("127.0.0.1");
    connectionFactory.setUsername("guest");
    connectionFactory.setPassword("guest");
    return connectionFactory;
  }

  @Bean
  public Queue queue() {
    return new Queue(RABBIT_MESSAGE_QUEUE, true);
  }

  @Bean
  public DirectExchange exchange() {
    return new DirectExchange(RABBIT_MESSAGE_EXCHANGE);
  }

  @Bean
  Binding exchangeBinding(DirectExchange directExchange, Queue queue) {
    return 
    BindingBuilder.bind(queue).to(directExchange).with(ROUTING_KEY);
  }

  @Bean
  public RabbitAdmin rabbitAdmin() {
    RabbitAdmin admin = new RabbitAdmin(connectionFactory());
    admin.declareQueue(queue());
    admin.declareExchange(exchange());
    admin.declareBinding(exchangeBinding(exchange(), queue()));
    return admin;
  }

  @Bean
  public MessageConverter messageConverter() {
    return new Jackson2JsonMessageConverter();
  }

  @Bean
  public RabbitTemplate rabbitTemplate() {
    RabbitTemplate template = new RabbitTemplate(connectionFactory());
    template.setRoutingKey(ROUTING_KEY);
    template.setExchange(RABBIT_MESSAGE_EXCHANGE);
    template.setMessageConverter(messageConverter());
    return template;
  }

  @Bean
  public SimpleRabbitListenerContainerFactory listenerContainer() {
    SimpleRabbitListenerContainerFactory factory = new 
    SimpleRabbitListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory());
    factory.setMaxConcurrentConsumers(5);
    return factory;
  }

}

创建消息接收器

现在,我们将创建一个带有@RabbitListener注释方法的Consumer监听器类,该方法将从 RabbitMQ 接收消息:

@Service
public class Consumer {

  private static final Logger LOGGER = 
  Logger.getLogger(Consumer.class);

  @RabbitListener(containerFactory = "listenerContainer",
  queues = RabbitMqConfiguration.RABBIT_MESSAGE_QUEUE)
  public void onMessage(Message message) {
      LOGGER.info("Received Message: " + 
      new String(message.getBody()));
    }
}

这是消息listenerContainer类。每当生产者向queue发送消息时,这个类将接收到它,只有带有@RabbitListener(containerFactory = "listenerContainer", queues = RabbitMqConfiguration.RABBIT_MESSAGE_QUEUE)注解的方法才会接收到消息。在这个注解中,我们提到了containerFactory属性,它指向了在listenerContainer bean 中定义的消息监听器工厂。

创建消息生产者

为了运行这个应用程序,我们将使用RabbitTemplate.convertAndSend()方法来发送消息。这个方法还将自定义的 Java 对象转换为 AMQP 消息,并发送到直接交换。以下BankAccount类被创建为一个自定义类来填充消息属性:

public class BankAccount {

    private int accountId;
    private String accountType;

    public BankAccount(int accountId, String accountType) {
        this.accountId = accountId;
        this.accountType = accountType;
    }

    public int getAccountId() {
        return accountId;
    }

    public String getAccountType() {
        return accountType;
    }

    @Override
    public String toString() {
        return "BankAccount{" +
                "Account Id=" + accountId +
                ", Account Type='" + accountType + '\'' +
                '}';
    }
}

在下一个类中,我们将使用一些适当的值初始化前面的类,并使用RabbitTemplate.convertAndSend()将其发送到交换:

public class Producer {

  private static final Logger LOGGER = 
  Logger.getLogger(Producer.class);

  @SuppressWarnings("resource")
  public static void main(String[] args) {
        ApplicationContext ctx = new 
        AnnotationConfigApplication
        Context(RabbitMqConfiguration.class);
        RabbitTemplate rabbitTemplate = 
        ctx.getBean(RabbitTemplate.class);
        LOGGER.info("Sending bank account information....");
        rabbitTemplate.convertAndSend(new BankAccount(100, "Savings 
        Account"));
        rabbitTemplate.convertAndSend(new BankAccount(101, "Current 
        Account"));

    }

}

当我们运行上述代码时,生产者将使用convertAndSend()方法发送两个BankAccount对象,并显示以下输出:

2018-05-13 19:46:58 INFO Producer:17 - Sending bank account information....
2018-05-13 19:46:58 INFO Consumer:17 - Received Message: {"accountId":100,"accountType":"Savings Account"}
2018-05-13 19:46:58 INFO Consumer:17 - Received Message: {"accountId":101,"accountType":"Current Account"}

最大化 RabbitMQ 的吞吐量

以下是与最大消息传递吞吐量相关的最佳性能配置选项:

  • 保持队列短

  • 避免使用懒惰队列

  • 避免持久化消息

  • 创建多个队列和消费者

  • 将队列分成不同的核心

  • 禁用确认

  • 禁用不必要的插件

RabbitMQ 的性能和可伸缩性

有许多重要的要点,我们应该考虑实现与 RabbitMQ 的最佳性能:

  • 有效载荷消息大小

  • 交换管理

  • 正确配置预取

  • RabbitMQ HiPE

  • 节点的集群

  • 禁用 RabbitMQ 统计信息

  • 更新 RabbitMQ 库

总结

在本章中,我们学习了消息传递的概念。我们还了解了使用消息系统的优势。我们学习了 AMQP。我们通过理解 JMS API 问题了解了 AMQP 的需求。我们还看到了 AMQP 和 JMS API 之间的区别。我们学习了与 AMQP 相关的交换、队列和绑定。我们还学习了 RabbitMQ 的设置方面以及与 Spring 应用程序相关的不同配置。

在下一章中,我们将学习 Java 线程的核心概念,然后我们将转向java.util.concurrent包提供的高级线程支持。我们还将学习java.util.concurrent的各种类和接口。我们将学习如何使用 Java 线程池来提高性能。我们将学习 Spring 框架提供的有用功能,如任务执行、调度和异步运行。最后,我们将研究 Spring 事务管理与线程以及线程的各种最佳编程实践。

第八章:多线程和并发编程

在上一章中,我们学习了如何优化 Spring 消息传递。我们还学习了各种配置技巧,帮助我们提高应用程序的性能。我们还研究了监视和配置 JMS 和 RabbitMQ 以实现最佳性能。

在本章中,我们将介绍 Java 线程的核心概念,然后将转向java.util.concurrent包提供的高级线程支持。对于这个包,我们将看到各种类和接口,帮助我们编写多线程和并发编程。我们还将学习如何使用 Java 线程池来提高性能。我们将介绍 Spring 框架提供的有用功能,如任务执行、调度和异步运行。最后,我们将探讨 Spring 事务管理与线程以及线程的各种最佳编程实践。

本章将涵盖以下主题:

  • Java 经典线程

  • java.util.concurrent

  • 使用线程池进行异步处理

  • Spring 任务执行和调度

  • Spring 异步

  • Spring 和线程-事务

  • Java 线程最佳编程实践

Java 经典线程

Java 应用程序通过线程执行,线程是程序内部的独立执行路径。任何 Java 程序至少有一个线程,称为主线程,由 Java 虚拟机(JVM)创建。Java 是一个多线程应用程序,允许在任何特定时间执行多个线程,并且这些线程可以并发地运行,无论是异步还是同步。当多个线程执行时,每个线程的路径可以与其他线程的路径不同。

JVM 为每个线程提供自己的堆栈,以防止线程相互干扰。单独的堆栈帮助线程跟踪它们要执行的下一个指令,这可以与其他线程不同。堆栈还为线程提供了方法参数、局部变量和返回值的副本。

线程存在于一个进程中,并与进程的其他线程共享资源,如内存和打开的文件。在不同线程之间共享资源的能力使它们更容易受到性能要求的影响。在 Java 中,每个线程都是由java.lang.Thread类和java.lang.Runnable接口创建和控制的。

创建线程

线程是 Java 语言中的对象。可以使用以下机制创建线程:

  • 创建一个实现Runnable接口的类

  • 创建一个扩展Thread类的类

有两种创建Runnable对象的方法。第一种方法是创建一个实现Runnable接口的类,如下所示:

public class ThreadExample {
  public static void main(String[] args) {
    Thread t = new Thread(new MyThread());
    t.start();
  }
}
class MyThread implements Runnable {
  private static final Logger LOGGER =     
  Logger.getLogger(MyThread.class);
  public void run() {
    //perform some task
    LOGGER.info("Hello from thread...");
  }
}

在 Java 8 之前,我们只能使用这种方式创建Runnable对象。但自 Java 8 以来,我们可以使用 Lambda 表达式创建Runnable对象。

创建Runnable对象后,我们需要将其传递给接受Runnable对象作为参数的Thread构造函数:

Runnable runnable = () -> LOGGER.info("Hello from thread...");
Thread t = new Thread(runnable);

有些构造函数不接受Runnable对象作为参数,比如Thread()。在这种情况下,我们需要采取另一种方法来创建线程:

public class ThreadExample1 {
  public static void main(String[] args) {
    MyThread t = new MyThread1();
    t.start();
  }

}
class MyThread1 extends Thread {
  private static final Logger LOGGER = 
  Logger.getLogger(MyThread1.class);
  public void run() {
    LOGGER.info("Hello from thread...");
  }
}

线程生命周期和状态

在处理线程和多线程环境时,了解线程生命周期和状态非常重要。在前面的例子中,我们看到了如何使用Thread类和Runnable接口创建 Java 线程对象。但是要启动线程,我们必须首先创建线程对象,并调用其start()方法来执行run()方法作为线程。

以下是 Java 线程生命周期的不同状态:

  • New:使用new运算符创建线程时,线程处于新状态。在这个阶段,线程还没有启动。

  • 可运行:当我们调用线程对象的start()方法时,线程处于可运行状态。在这个阶段,线程调度程序仍然没有选择它来运行。

  • 运行:当线程调度程序选择了线程时,线程状态从可运行变为运行。

  • 阻塞/等待:当线程当前不具备运行资格时,线程状态为阻塞/等待。

  • 终止/死亡:当线程执行其运行方法时,线程状态被终止/死亡。在这个阶段,它被认为是不活动的。

更高级的线程任务

我们已经看到了线程的生命周期和其状态,但线程也支持一些高级任务,比如睡眠、加入和中断。让我们讨论一下:

  • 睡眠sleep()线程方法可以用来暂停当前线程的执行,指定的时间量。

  • 加入join()线程方法可以用来暂停当前线程的执行,直到它加入的线程完成其任务。

  • 中断interrupt()线程方法可以用来打破线程的睡眠或等待状态。如果线程处于睡眠或等待状态,它会抛出InterruptedException,否则,它不会中断线程,但会将中断标志设置为 true。

同步线程

在多线程应用程序中,可能会出现多个线程尝试访问共享资源并产生错误和意外结果的情况。我们需要确保资源只能被一个线程使用,这可以通过同步来实现。synchronized关键字用于实现同步;当我们在 Java 中定义任何同步块时,只有一个线程可以访问该块,其他线程被阻塞,直到在该块内的线程退出该块。

synchronized关键字可以与以下不同类型的块一起使用:

  • 实例方法

  • 静态方法

  • 实例方法内的代码块

  • 静态方法内的代码块

在 Java 中,同步块会降低性能。我们必须在需要时使用synchronized关键字,否则,我们应该只在需要的关键部分使用同步块。

多线程问题

多线程是一种非常强大的机制,可以帮助我们更好地利用系统资源,但在读写多个线程共享的数据时,我们需要特别小心。多线程编程有两个基本问题——可见性问题和访问问题。可见性问题发生在一个线程的效果可以被另一个线程看到时。访问问题可能发生在多个线程同时访问相同的共享资源时。

由于可见性和访问问题,程序不再做出反应,导致死锁或生成不正确的数据。

java.util.concurrent 包

在前一节中,我们专注于 Java 对线程的低级支持。在本节中,我们将继续查看java.util.concurrent包提供的 Java 高级线程支持。这个包有各种类和接口,提供非常有用的功能,帮助我们实现多线程和并发编程。在本节中,我们将主要关注这个包的一些最有用的实用工具。

以下图表显示了java.util.concurrent API 的高级概述:

让我们详细讨论接口。

执行者

Executor提供了一个抽象层,用于管理所有内部线程管理任务,并管理线程的整个并发执行流程。Executor是一个执行提供的任务的对象。

Java 并发 API 提供了以下三个基本接口用于执行者:

  • Executor:这是一个简单的接口,用于启动一个新任务。它不严格要求执行是异步的。

  • ExecutorService:这是Executor接口的子接口。它允许我们异步地将任务传递给线程执行。它提供了管理先前提交的任务终止的方法,如shutdown()shutdownNow()awaitTermination(long timeout, TimeUnit unit)。它还提供了返回Future对象以跟踪一个或多个异步任务进度的方法。

  • ScheduledExecutorService:这是ExecutorService的子接口。它提供了各种关键方法,如schedule()scheduleAtFixedRate()scheduleWithFixedDelay()。所有调度方法都可以接受相对延迟和周期作为参数,这有助于我们安排任务在给定延迟或周期后执行。

以下是一个简单示例,演示了如何创建Executor以执行Runnable任务:

public class ExecutorExample {
    private static final Logger LOGGER = 
    Logger.getLogger(ExecutorExample.class);

    public static void main(String[] args) {
        ExecutorService pool = Executors.newSingleThreadExecutor();

            Runnable task = new Runnable() {
            public void run() {
                LOGGER.info(Thread.currentThread().getName());
            }
        }; 

        pool.execute(task); 
        pool.shutdown();
    }
}

在前面的示例中,通过匿名类创建了一个Runnable对象,并通过单线程Executor接口执行任务。当我们编译和运行上述类时,将得到以下输出:

pool-1-thread-1

ThreadFactory

ThreadFactory接口用于按需创建新线程,还帮助我们消除创建线程的大量样板代码。

以下示例显示了如何使用ThreadFactory接口创建新线程:

public class ThreadFactoryExample implements ThreadFactory {
  private static final Logger LOGGER =   
  Logger.getLogger(ThreadFactoryExample.class);

  public static void main(String[] args) {
    ThreadFactoryExample factory = new ThreadFactoryExample();

    Runnable task = new Runnable() {
      public void run() {
        LOGGER.info(Thread.currentThread().getName());
      }
    };
    for (int i = 0; i < 5; i++) {
      Thread t = factory.newThread(task);
      t.start();
    }
  }

  @Override
  public Thread newThread(Runnable r) {
    Thread t = new Thread(r);
    return t;
  }
}

当我们编译和运行上述类时,将得到以下输出:

Thread-0
Thread-1

同步器

Java 提供了synchronized关键字来编写同步代码,但仅通过synchronized关键字正确编写同步代码是困难的。java.util.concurrent包提供了各种实用程序类,如CountDownLatchCyclicBarrierExchangerSemaphorePhaser,它们被称为同步器。同步器是提供线程同步的并发实用程序,而无需使用wait()notify()方法。让我们看看以下类:

  • CountDownLatch:这允许一个线程在一个或多个线程完成之前等待。

  • CyclicBarrier:这与CountdownLatch非常相似,但它允许多个线程在开始处理之前等待彼此。

  • 信号量:这维护了一组许可证,用于限制可以访问共享资源的线程数量。线程在访问共享资源之前需要从信号量获取许可证。它提供了两个主要方法acquire()release(),分别用于获取和释放许可证。

  • Exchanger:这提供了一个同步点,线程可以在其中交换对象。

  • Phaser:这提供了类似于CyclicBarrierCountDownLatch的线程同步机制,但支持更灵活的使用。它允许一组线程在障碍上等待,然后在最后一个线程到达后继续,并且还支持多个执行阶段。

并发集合类

并发集合类提供了比其他集合类(如HashMapHashtable)更好的可伸缩性和性能。以下是java.util.concurrent包中提供的有用并发类:

  • ConcurrentHashMap:这类似于HashMapHashtable,但它被设计为在并发编程中工作,而无需显式同步。HashtableConcurrentHashMap都是线程安全的集合,但ConcurrentHashMapHashtable更先进。它不会锁定整个集合进行同步,因此在有大量更新和较少并发读取时非常有用。

  • BlockingQueue:生产者-消费者模式是异步编程中最常见的设计模式,BlockingQueue数据结构在这些异步场景中非常有用。

  • DelayQueue:这是一个无限大小的阻塞队列,其中的元素只有在其延迟到期时才能被取出。如果多个元素延迟到期,那么延迟到期时间最长的元素将首先被取出。

Lock接口提供了比synchronized块更高级的锁定机制。synchronized块和Lock之间的主要区别是synchronized块完全包含在一个方法中,而Lock接口有单独的lock()unlock()方法,可以在不同的方法中调用。

可调用和未来

Callable接口类似于Runnable对象,但它可以返回任何类型的对象,这有助于我们从Callable任务中获取结果或状态。

Callable任务返回Future对象,用于获取异步操作的结果。它的用途包括提供一对方法来检查异步执行是否已完成,并检索计算的结果。

原子变量

原子变量是在java.util.concurrent.atomic包中引入的非阻塞算法。使用原子变量的主要好处是我们不需要担心同步。在多线程环境中,原子变量是避免数据不一致的必要性。它支持对单个变量进行无锁、线程安全的操作。

使用线程池进行异步处理

线程池是多线程编程中的核心概念,用于提供一组空闲线程,可用于执行任务。线程池可以重用先前创建的线程来执行当前任务,以便在请求到达时线程已经可用,这可以减少线程创建的时间并提高应用程序的性能。通常,线程池可以用于 Web 服务器来处理客户端请求,还可以维护到数据库的开放连接。

我们可以配置池中并发线程的最大数量,这对于防止过载很有用。如果所有线程都在执行任务,那么新任务将被放置在队列中,等待线程可用。

Java 并发 API 支持以下类型的线程池:

  • 固定线程池:具有固定数量线程的线程池。只有在有线程可用时任务才会执行,否则会在队列中等待。使用Executors.newFixedThreadPool()方法来创建固定线程池。

  • 缓存线程池:我们可以根据需要创建新线程,但也可以重用先前创建的线程。如果线程空闲了 60 秒,它将被终止并从池中移除。使用Executors.newCachedThreadPool()方法来创建缓存线程池。

  • 单线程池:一个线程的线程池。它逐个执行任务。使用Executors.newSingleThreadExecutor()方法来创建单线程池。

  • 分支/合并池:用于更快地执行重型任务的线程池,通过递归地将任务分割成较小的片段。要创建分支/合并池,我们需要创建ForkJoinPool类的实例。

以下是固定线程池的一个简单示例:

public class ThreadPoolExample {
  private static final Logger LOGGER = 
  Logger.getLogger(ThreadPoolExample.class);
  public static void main(String[] args) {
    ExecutorService executor = Executors.newFixedThreadPool(3);

    for (int i = 1; i <= 6; i++) {
      Runnable task = new Task(" " + i);
      executor.execute(task);
    }
    executor.shutdown();
    while (!executor.isTerminated()) {
    }
    LOGGER.info("All threads finished");
  }
}

以下演示了任务的实现方式:

public class Task implements Runnable {
  private static final Logger LOGGER = Logger.getLogger(Task.class);
  private String taskNumber;

  public Task(String taskNumber) {
    this.taskNumber = taskNumber;
  }

  @Override
  public void run() {
    LOGGER.info(Thread.currentThread().getName() + ", Execute Task = " 
    + taskNumber);
    taskProcess();
    LOGGER.info(Thread.currentThread().getName() + ", End");
  }

  private void taskProcess() {
    try {
      Thread.sleep(2000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

在前面的示例中,我们创建了一个最多有三个并发线程的池,并向executor对象提交了6个任务。当我们编译和运行前面的类时,我们知道只有三个线程执行任务。

以下是输出:

pool-1-thread-1, Execute Task = 1
pool-1-thread-2, Execute Task = 2
pool-1-thread-3, Execute Task = 3
pool-1-thread-1, End
pool-1-thread-1, Execute Task = 4
pool-1-thread-3, End
pool-1-thread-2, End
pool-1-thread-2, Execute Task = 5
pool-1-thread-3, Execute Task = 6
pool-1-thread-1, End
pool-1-thread-2, End
pool-1-thread-3, End
All threads finished

Spring 任务执行和调度

在任何 Web 应用程序中使用线程处理长时间运行的任务并不容易。有时,我们需要异步运行任务或在特定延迟后运行任务,这可以通过 Spring 的任务执行和调度来实现。Spring 框架引入了用于异步执行和任务调度的抽象,使用TaskExecutorTaskScheduler接口。

TaskExecutor

Spring 提供了TaskExecutor接口作为处理Executor的抽象。TaskExecutor的实现类如下:

  • SimpleAsyncTaskExecutor:这启动一个新线程并异步执行。它不重用线程。

  • SyncTaskExecutor:这在调用线程中同步执行每个任务。它不重用线程。

  • ConcurrentTaskExecutor:这公开了用于配置java.util.concurrent.Executor的 bean 属性。

  • SimpleThreadPoolTaskExecutor:这是QuartzSimpleThreadPool的子类,它监听 Spring 的生命周期回调。

  • ThreadPoolTaskExecutor:这公开了用于配置java.util.concurrent.ThreadPoolExecutor的 bean 属性,并将其包装在TaskExecutor中。

  • TimerTaskExecutor:这实现了一个TimerTask类作为其后备实现。它在单独的线程中同步执行方法。

  • WorkManagerTaskExecutor:这使用CommonJWorkManager接口作为其后备实现。

让我们看一个在 Spring 应用程序中使用SimpleAsyncTaskExecutor执行任务的简单示例。它为每个任务提交创建一个新线程并异步运行。

这是配置文件:

@Configuration
public class AppConfig {
  @Bean
  AsyncTask myBean() {
    return new AsyncTask();
  }
  @Bean
  AsyncTaskExecutor taskExecutor() {
    SimpleAsyncTaskExecutor t = new SimpleAsyncTaskExecutor();
    return t;
  }
}

这是一个 bean 类,我们已经将5个任务分配给了TaskExecutor

public class AsyncTask {
  @Autowired
  private AsyncTaskExecutor executor;
  public void runTasks() throws Exception {
    for (int i = 1; i <= 5; i++) {
      Runnable task = new Task(" " + i);
      executor.execute(task);
    }
  }
}

以下是从main方法执行任务的代码:

public class TaskExecutorExample {
  public static void main(String[] args) throws Exception {
    ApplicationContext context = new 
    AnnotationConfigApplicationContext(AppConfig.class);
    AsyncTask bean = context.getBean(AsyncTask.class);
    bean.runTasks();
  }
}

当我们编译并运行上述类时,将得到以下输出。在这里,我们可以看到创建了五个线程,并且它们异步执行任务:

SimpleAsyncTaskExecutor-1, Execute Task = 1
SimpleAsyncTaskExecutor-4, Execute Task = 4
SimpleAsyncTaskExecutor-3, Execute Task = 3
SimpleAsyncTaskExecutor-2, Execute Task = 2
SimpleAsyncTaskExecutor-5, Execute Task = 5
SimpleAsyncTaskExecutor-2, End
SimpleAsyncTaskExecutor-1, End
SimpleAsyncTaskExecutor-4, End
SimpleAsyncTaskExecutor-3, End
SimpleAsyncTaskExecutor-5, End

TaskScheduler

有时,我们需要按固定间隔执行任务,这可以通过 Spring 调度程序框架实现。在本节中,我们将看到如何使用一些注解在 Spring 中安排任务。

让我们看一个在 Spring 应用程序中安排任务的简单示例:

@Configuration
@EnableScheduling
public class SpringSchedulingExample {
    private static final Logger LOGGER =                                                     
    Logger.getLogger(SpringSchedulingExample.class);
    @Scheduled(fixedDelay = 2000)
    public void scheduledTask() {
        LOGGER.info("Execute task " + new Date());
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new 
        AnnotationConfigApplicationContext(
        SpringSchedulingExample.class);
        String scheduledAnnotationProcessor =         
        "org.springframework.context.annotation.
        internalScheduledAnnotationProcessor";
        LOGGER.info("ContainsBean : " + scheduledAnnotationProcessor + 
        ": " + context.containsBean(scheduledAnnotationProcessor));
        try {
            Thread.sleep(12000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            context.close();
        }
    }
} 

在 Spring 中,我们可以通过@EnableScheduling注解启用任务调度。一旦启用任务调度,Spring 将自动注册一个内部 bean 后处理器,该处理器将在 Spring 管理的 bean 上找到@Scheduled注解的方法。

在上一个示例中,我们使用@Scheduled注解将scheduledTask()方法与fixedDelay属性一起注释,以便每2秒调用一次。我们还可以使用其他属性,如fixedRatecron

@Scheduled(fixedRate = 2000)
@Scheduled(cron = "*/2 * * * * SAT,SUN,MON")

当我们编译并运行上一个类时,将得到以下输出:

Execute task Thu May 10 20:18:04 IST 2018
ContainsBean : org.springframework.context.annotation.internalScheduledAnnotationProcessor: true
Execute task Thu May 10 20:18:06 IST 2018
Execute task Thu May 10 20:18:08 IST 2018
Execute task Thu May 10 20:18:10 IST 2018
Execute task Thu May 10 20:18:12 IST 2018
Execute task Thu May 10 20:18:14 IST 2018

Spring Async

在本节中,我们将看到 Spring 中的异步执行支持。在某些情况下,我们需要异步执行一些任务,因为该任务的结果不需要用户,所以我们可以在单独的线程中处理该任务。异步编程的主要好处是我们可以提高应用程序的性能和响应能力。

Spring 通过@EnableAsync@Async提供了异步方法执行的注解支持。让我们详细讨论它们。

@EnableAsync 注解

我们可以通过简单地将@EnableAsync添加到配置类来启用异步处理,如下所示:

@Configuration
@EnableAsync
public class AppConfig {
  @Bean
  public AsyncTask asyncTask() {
    return new AsyncTask();
  }
}

在上面的代码中,我们没有将TaskExecutor作为 bean 提供,因此 Spring 将隐式地使用默认的SimpleAsyncTaskExecutor

@Async 注解

一旦启用了异步处理,那么用@Async注解标记的方法将异步执行。

以下是@Async注解的简单示例:

public class AsyncTask {
  private static final Logger LOGGER = 
  Logger.getLogger(AsyncTask.class);
  @Async
  public void doAsyncTask() {
    try {
      LOGGER.info("Running Async task thread : " + 
      Thread.currentThread().getName());
    } catch (Exception e) {
    }
  }
}

我们还可以将@Async注解添加到具有返回类型的方法中,如下所示:

@Async
  public Future<String> doAsyncTaskWithReturnType() {
    try 
    {
      return new AsyncResult<String>("Running Async task thread : " + 
      Thread.currentThread().getName());
    } 
    catch (Exception e) { 
    }
    return null;
  }

在上面的代码中,我们使用了实现FutureAsyncResult类。这可以用于获取异步方法执行的结果。

以下是从main方法调用异步方法的代码:

public class asyncExample {
  private static final Logger LOGGER = 
  Logger.getLogger(asyncExample.class);
  public static void main(String[] args) throws InterruptedException {
    AnnotationConfigApplicationContext ctx = new 
    AnnotationConfigApplicationContext();
    ctx.register(AppConfig.class);
    ctx.refresh();
    AsyncTask task = ctx.getBean(AsyncTask.class);
    LOGGER.info("calling async method from thread : " + 
    Thread.currentThread().getName());
    task.doAsyncTask();
    LOGGER.info("Continue doing something else. ");
    Thread.sleep(1000);
  }
}

当我们编译并运行上述类时,将得到以下输出:

calling async method from thread : main
Continue doing something else. 
Running Async Task thread : SimpleAsyncTaskExecutor-1

@Async 与 CompletableFuture

在上一节中,我们看到了如何使用java.util.Future来获取异步方法执行的结果。它提供了一个isDone()方法来检查计算是否完成,以及一个get()方法在计算完成时检索计算结果。但是使用Future API 存在一定的限制:

  • 假设我们编写了代码,通过远程 API 从电子商务系统中获取最新的产品价格。这个任务很耗时,所以我们需要异步运行它,并使用Future来获取该任务的结果。现在,当远程 API 服务宕机时,问题就会出现。这时,我们需要手动完成Future,使用产品的最后缓存价格,这是Future无法实现的。

  • Future只提供一个get()方法,当结果可用时通知我们。我们无法将回调函数附加到Future,并在Future结果可用时自动调用它。

  • 有时我们有需求,比如需要将长时间运行任务的结果发送给另一个长时间运行的任务。我们无法使用Future创建这样的异步工作流。

  • 我们无法并行运行多个Future

  • Future API 没有任何异常处理。

由于这些限制,Java 8 引入了比java.util.Future更好的抽象,称为CompletableFuture。我们可以使用以下无参构造函数简单地创建CompletableFuture

CompletableFuture<String> completableFuture = new CompletableFuture<String>();

以下是CompletableFuture提供的方法列表,帮助我们解决Future的限制:

  • complete()方法用于手动完成任务。

  • runAsync()方法用于异步运行不返回任何内容的后台任务。它接受一个Runnable对象并返回CompletableFuture<Void>

  • supplyAsync()方法用于异步运行后台任务并返回一个值。它接受Supplier<T>并返回CompletableFuture<T>,其中T是供应商提供的值的类型。

  • thenApply()thenAccept()thenRun()方法用于将回调附加到CompletableFuture

  • thenCompose()方法用于将两个依赖的CompletableFuture组合在一起。

  • thenCombine()方法用于将两个独立的CompletableFuture组合在一起。

  • allOf()anyOf()方法用于将多个CompletableFuture组合在一起。

  • exceptionally()方法用于从Future获取生成的错误。我们可以记录错误并设置默认值。

  • handle()方法用于处理异常。

Spring 和线程-事务

Spring 框架为数据库事务管理提供了广泛的 API。Spring 负责所有基本的事务管理控制,并为不同的事务 API 提供了一致的编程模型,如 JDBC、Hibernate、Java Transaction API(JTA)、Java Persistence API(JPA)和 Java Data Objects(JDO)。Spring 提供了两种类型的事务:一种是声明式的,另一种是编程式的事务管理。声明式的层次很高,而编程式的更高级但更灵活。

Spring 事务管理在单个线程中运行得很好。但它无法管理跨多个线程的事务。如果我们尝试在多个线程中使用事务,我们的程序会给出运行时错误或意外结果。

要理解为什么 Spring 事务在多个线程中失败,首先,我们需要了解 Spring 如何处理事务。Spring 将所有事务信息存储在org.springframework.transaction.support.TransactionSynchronizationManager类内的ThreadLocal变量中:

public abstract class TransactionSynchronizationManager {
  private static final Log logger =         
  LogFactory.getLog(TransactionSynchronizationManager.class);
  private static final ThreadLocal<Map<Object, Object>> resources = new  
  NamedThreadLocal("Transactional resources");
  private static final ThreadLocal<Set<TransactionSynchronization>> 
  synchronizations = new NamedThreadLocal("Transaction 
  synchronizations");
  private static final ThreadLocal<String> currentTransactionName = new 
  NamedThreadLocal("Current transaction name");
  private static final ThreadLocal<Boolean> currentTransactionReadOnly 
  = new NamedThreadLocal("Current transaction read-only status");
  private static final ThreadLocal<Integer> 
  currentTransactionIsolationLevel = new NamedThreadLocal("Current 
  transaction isolation level");
  private static final ThreadLocal<Boolean> actualTransactionActive = 
  new NamedThreadLocal("Actual transaction active");
}

线程的局部变量仅保存特定事务的信息,仅限于单个线程,不能被另一个线程访问。因此,正在进行的事务信息不会传递给新创建的线程。结果将是一个错误,指示事务丢失。

现在我们能够理解 Spring 事务在多个线程中的问题。Spring 无法将事务状态保持到旧线程,以便从新创建的线程中访问。为了解决多线程的事务问题,我们需要手动将线程的局部变量值传递给新创建的线程。

Java 线程最佳编程实践

使用多线程和并发编程的目的是提高性能,但我们需要始终记住速度是在正确性之后。Java 编程语言从语言到 API 级别提供了大量的同步和并发支持,但这取决于个人在编写无错误的 Java 并发代码方面的专业知识。以下是 Java 并发和多线程的最佳实践,这有助于我们在 Java 中编写更好的并发代码:

  • 使用不可变类:在多线程编程中,我们应该始终优先使用不可变类,因为不可变类确保在操作中值不会在没有使用同步块的情况下更改。例如,在不可变类中,如java.lang.String,对String的任何修改,如添加内容或转换为大写,总是创建另一个字符串对象,保持原始对象不变。

  • 使用局部变量:始终尝试使用局部变量而不是实例或类级变量,因为局部变量永远不会在线程之间共享。

  • 使用线程池:线程池可以重用先前创建的线程,并消除线程创建的时间,从而提高应用程序的性能。

  • 使用同步工具:在这里,我们可以使用同步工具而不是waitnotify方法。java.util.concurrent包提供了更好的同步工具,如CycicBariierCountDownLatchSempahoreBlockingQueue。使用CountDownLatch等待五个线程完成任务比使用waitnotify方法实现相同的工具更容易。使用BlockingQueue更容易实现生产者-消费者设计,而不是使用waitnotify方法。

  • 使用并发集合而不是同步集合:并发集合是使用Lock接口提供的新锁定机制实现的,并且设计成这样,我们可以利用底层硬件和 JVM 提供的本机并发构造。并发集合比它们的同步对应物具有更好的可伸缩性和性能。如果有许多并发更新和较少读取,ConcurrentHashMap比同步的HashMapHashtable类提供更好的性能。

  • 最小化锁定范围:我们应该尽量减少锁定范围,因为锁定块不会同时执行,并且会影响应用程序的性能。如果我们的需求无法满足,首先尝试使用原子和易失性变量来实现我们的同步需求,然后需要使用Lock接口提供的功能。我们还可以减少锁定范围,使用同步块而不是同步方法。

  • 使用 Java Executor 框架:它在 Java 线程框架上提供了一个抽象层,并在多线程环境中创建和执行线程方面提供了更好的控制。

摘要

在这一章中,我们探讨了 Java 线程,并学习了如何利用java.util.concurrent包实现多线程和并发编程。我们还学习了如何在应用程序中使用线程池来提高性能。我们看到了 Spring 提供的任务执行和调度功能,还学习了 Spring 的@Async支持,可以提高应用程序的性能和响应能力。我们回顾了 Spring 事务管理在处理多线程时可能出现的问题,并了解了多线程和并发编程的最佳编程实践。

在下一章中,我们将学习如何对应用程序进行性能分析,以找出性能问题。这对于识别性能问题非常有用。我们还将学习日志记录,这是识别应用程序问题的重要工具。

第九章:性能分析和日志记录

在上一章中,我们深入研究了多线程和并发编程的细节。我们查看了java.util.concurrent包的 API。本章涵盖了用于异步编程的线程池、Spring 任务执行、调度和 Spring Async API。在本章的后半部分,我们将 Spring Async 与CompletableFuture进行了比较。

在类似的情况下,本章将重点关注分析和日志记录。本章首先定义了分析和日志记录,以及它们如何有助于评估应用程序性能。在本章的后半部分,重点将放在学习可以用来研究应用程序性能的软件工具上。

本章将涵盖以下主题:

  • 性能分析

  • 应用程序日志记录和监控

  • 性能分析工具

性能分析

本节将重点关注性能和应用程序性能分析。分析是应用程序开发和部署生命周期中的重要步骤。它帮助我们执行以下两件事:

  1. 定义预期性能结果的基准

  2. 衡量并比较当前性能结果与基准

第二步定义了进一步的行动,以将性能提升到基准水平。

应用程序性能

性能在软件应用程序方面对不同的人有不同的含义。它必须有一些上下文才能更好地理解。应用程序性能根据两组性能指标进行衡量。应用程序用户实际观察或体验到的性能仍然是衡量应用程序性能的最重要指标之一。这包括在高峰和正常负载期间的平均响应时间。与平均响应时间相关的测量包括应用程序响应用户操作(例如页面刷新、导航或按钮点击)所需的时间。它们还包括执行某些操作(例如排序、搜索或加载数据)所需的时间。

本节旨在为技术团队提供一些配置和内部方面的视角,这些配置和内部方面可以进行设置或更改,以优化效果,从而提高应用程序的性能。通常情况下,技术团队在没有遇到性能问题时很少关注应用程序使用的内存或 CPU 利用率。应用程序事务包括应用程序每秒接收的请求、每秒数据库事务和每秒提供的页面。系统的负载通常是以应用程序处理的交易量来衡量的。

还有另一组测量,涉及测量应用程序在执行操作时所利用的计算资源。这是一个很好的方法,可以确定应用程序是否有足够的资源来承受给定的负载。它还有助于确定应用程序是否利用的资源超出了预期。如果是这样,可以得出结论应用程序在性能方面没有进行优化。云托管应用程序如今很受欢迎。在这个时代,用户在云端部署的应用程序、非云基础设施上以及本地环境上应该有相同的体验是很重要的。

只要应用程序按预期运行,应用程序性能监控和改进可能并不是必要的。然而,在应用程序开发生命周期的一部分,会出现新的需求,添加新功能,并且应用程序变得日益复杂。这开始影响应用程序的性能,因为主要关注点放在了新功能开发上。当性能达不到标准时,因为没有人真正致力于应用程序性能的提升。

应用程序日志记录和监控

本节重点关注应用程序运行时记录重要信息。它有助于从各个方面调试应用程序,我们将详细了解。本节涵盖的另一个重要方面是应用程序监控。在某些情况下,应用程序监控被认为与应用程序性能分析没有区别;这些在应用程序性能测量中肯定是不同的方面。

应用程序日志

在我们深入了解 Java 应用程序日志的细节之前,了解日志和记录是强制性的。日志是显示信息以帮助我们了解应用程序状态的语句。日志语句以应用程序特定的格式写入日志文件。日志语句可能包括诸如特定语句执行的日期和时间、各种变量的值以及对象的状态等信息。将日志语句写入日志文件的过程称为记录

每个应用程序都会出于各种目的生成日志。应用程序生成日志以跟踪应用程序事件,包括与访问相关的事件、登录和注销事件、应用程序发生错误时的事件以及系统配置修改。操作系统也会生成日志文件。日志文件可以被处理以分离所需的信息。记录是软件应用程序中最基本的部分之一。良好编写的日志和良好设计的记录机制对开发人员和管理员来说是巨大的实用工具。对于从事应用程序支持活动的团队来说,这是非常有用的。良好设计的记录可以为开发和支持团队节省大量时间。随着前端程序的执行,系统以一种隐形的方式构建日志文件。

以下是通常在应用程序中生成的常见日志文件:

  • 错误/异常日志:应用程序流程中的任何意外情况都被称为错误。错误可能出现的原因各不相同。错误根据严重性和对应用程序的影响进行分类。如果用户无法在应用程序中继续操作,这样的错误被归类为阻塞。如果网页没有适当的标签,它被归类为低严重性问题。错误日志是应用程序执行时发生的关键错误的记录。几乎不存在没有错误的应用程序。在 Java 中,不需要记录所有异常。Java 支持受控异常,可以加以处理并作为警告或错误消息抛出给用户。这可能是验证错误或用户输入错误,可以使用受控异常抛出。

  • 访问日志:在抽象层面上,任何发送到 Web 应用程序的请求都可以被视为对 Web 应用程序服务器上资源的请求。资源可以是 Web 页面、服务器上的 PDF 文件、图像文件或数据库中数据的报告。从安全性的角度来看,每个资源都必须受到访问权限的保护。访问权限定义了谁可以从 Web 应用程序访问资源。访问日志是关于谁尝试访问哪个资源的书面信息。它们还可能包括有关访问资源的位置的信息。访问日志为进入 Web 应用程序的每个请求写入访问信息。访问日志还可以用于查找有关访问者数量、首次访问应用程序的访问者数量、特定位置的访问者数量、特定页面的请求数量以及应用程序使用模式的信息。

  • 事务日志:事务与数据库相关。为了保持原子性和数据库完整性而执行的一系列数据库命令或语句被称为事务。事务用于保证在崩溃或故障时的保护。事务日志是记录或写入所有这些事务的文件。在特定时间,如果发现数据库不一致,那么事务日志在调试问题时会有所帮助。事务日志还可以用于记录执行的任何回滚操作。通常,事务日志还记录数据库语句的执行时间以及传递的参数。这些信息对于分析数据库性能问题非常有帮助。

  • 审计日志审计是检查应用程序的使用情况的过程。它检查正在使用的应用程序资源,访问或使用应用程序资源的用户以及用户的身份验证和授权信息。审计日志记录应用程序通过的每个事件,以及前面提到的详细信息。

日志记录最佳实践

在描述了应该记录的内容和常见的日志信息之后,本节详细介绍了日志记录的最佳实践:

  • 为每个日志语句分配适当的日志级别非常重要。

  • 在集群环境中也应考虑日志记录。我们可以使用相同类型的日志文件,文件名后缀为集群节点名称。这将防止在分析日志时覆盖或错误地考虑日志条目。

  • 构建日志文件会影响应用程序的性能。如果应用程序开始记录每个细小的信息,应用程序的性能将变慢。我们必须确保日志文件的大小和写入日志条目的频率较低。

  • 除了验证和输入错误之外,所有异常都必须记录。异常消息必须以清晰地突出问题的方式记录。最佳实践是让框架记录所有异常。

  • 日志必须用户友好且易于解析。日志可以以两种方式使用。一种方式是用户阅读日志以建立理解。另一种方式是实用程序根据日志格式解析应用程序日志,以过滤掉不重要的信息。

  • 每个日志条目必须与其他日志条目不同,尽管它们代表相同的信息。每个日志条目都可以有一个唯一的标识符,通常基于时间戳,可以用来区分它与其他日志。

  • 不应在日志文件中记录敏感信息。密码、凭据和身份验证密钥是一些例子。

在大多数情况下,最佳实践作为一般指导方针,并可以根据项目以定制化的方式进行遵循。

日志记录工具

在本章的前几节中,我们了解了日志记录的重要性。我们还学习了日志记录的最佳实践。现在是时候将日志记录工具添加到我们的技能集中了。本节重点介绍日志记录工具。日志记录工具很有帮助,因为它们提供了各种功能。在过去,日志文件由以纯文本格式编写的日志语句组成。纯文本日志文件在特定情况下仍然有用,比如分析基础设施数据,但它们已经不再足以记录应用程序的信息。Java 内置支持java.util.logging API 的标准日志记录。Log4j 是 Java 社区中另一个知名且广泛使用的日志记录工具。

在我们深入了解日志工具的细节之前,了解日志机制的关键要素是很重要的。以下是关键的日志记录组件:

  • 日志级别: Java 日志级别用于控制日志输出。它们提供了在启用或禁用各种日志级别方面的灵活性。这使得可以选择在日志文件中显示哪些日志。通过这种方式,可能在生产环境中运行的应用程序与在暂存环境中运行的相同应用程序具有不同的日志级别。启用一个级别的日志将使所有更高级别的日志在日志文件中打印。以下是 Java 日志记录 API 的日志级别和有效日志级别:
请求级别 有效日志级别
SEVERE WARNING
SEVERE
WARNING
INFO
CONFIG
FINE
FINER
FINEST
  • Logger: Logger对象的工作是记录应用程序消息。应用程序可以创建匿名记录器,这些记录器与Logger命名空间中的记录器存储方式不同。应用程序必须确保保留对Logger对象的引用,因为Logger可能随时被垃圾回收。Logger对象与父Logger对象相关联,父对象是Logger命名空间中最近的祖先。在记录过程中,日志消息被发送到Handler对象。Handler对象将日志消息转发到文件、日志或控制台。每个Logger对象都有与之关联的日志级别。它指示Logger将为其打印日志的最低级别。

  • 处理程序: Handler对象的责任是从Logger对象获取日志消息,并将这些日志消息发送到适当的目的地进行打印。例如,将日志消息写入控制台、将日志消息写入文件或将日志消息写入网络日志记录服务。可以启用或禁用Handler,从本质上讲,这会停止在输出介质上打印这些日志。

  • 格式化程序: 日志Formatter在将日志消息写入输出介质之前对其进行格式化。Java 支持两种类型的Formatter对象:SimpleFormatterXMLFormatterXMLFormatter对象需要在格式化记录周围包含头和尾。还可以创建自定义的Formatter对象。

  • LogManager: LogManager是一个单例对象,用于维护日志记录器和日志服务的共享状态。除此之外,LogManager对象还管理日志记录属性和Logger命名空间。LogManager对象在类初始化时被实例化。对象不能随后更改。LogManager默认从lib/logging.properties文件中读取初始配置,该文件可以进行修改。

以下图表显示了具有一个Handler的日志记录过程:

以下图表显示了具有多个处理程序的日志记录过程:

Java 标准日志记录

本节介绍了 Java 的内置日志记录机制。Java 日志记录 API 由java.util.logging包组成。核心包包括支持将纯文本或 XML 日志条目写入输出流、文件、内存、控制台或套接字。日志 API 还能够与操作系统上已存在的日志记录服务进行交互。

以下代码示例用于使用标准日志记录 API 打印日志消息:

package com.packt.springhighperformance.ch9.logging;

import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;

public class SampleLoggingOne {
  private static Logger logger = 
  Logger.getLogger("com.packt.springhighperformance.ch4.logging");

  public static void main(String argv[]) throws SecurityException, 
  IOException {
    FileInputStream fis = new FileInputStream("D:\\projects\\spring-    
    high-performance\\SampleProject\\src\\main\\resources
    \\logging.properties");
    LogManager.getLogManager().readConfiguration(fis);
    Timestamp tOne = new Timestamp(System.currentTimeMillis());
    for(int i=0; i < 100000; i++) {
        logger.fine("doing stuff");
    }
    Timestamp tTwo = new Timestamp(System.currentTimeMillis());
    System.out.println("Time: " + (tTwo.getTime() - tOne.getTime()));
    try {
      Bird.fly();
    } catch (Exception ex) {
      logger.log(Level.WARNING, "trouble flying", ex);
    }
    logger.fine("done");
  }
}

以下是前面示例中引用的logging.properties文件的示例:

# Logging
handlers = java.util.logging.ConsoleHandler
.level = ALL

# Console Logging
java.util.logging.ConsoleHandler.level = ALL

执行前面示例后的输出如下:

Feb 19, 2018 12:35:58 AM com.packt.springhighperformance.ch9.logging.SampleLoggingOne main
FINE: doing stuff
Feb 19, 2018 12:35:58 AM com.packt.springhighperformance.ch9.logging.SampleLoggingOne main
FINE: done

使用 Java 标准日志记录的好处是,您不需要安装项目中的单独的 JAR 依赖项。尽管日志记录与我们在服务器上遇到的故障排除问题有关,但我们还必须确保日志记录不会以负面方式影响应用程序性能。必须注意以下几点,以确保日志记录不会影响应用程序性能:

  • Logger.log方法用于通过Handler在输出介质上打印日志记录。我们可以使用Logger.isLoggable来确保Logger已启用日志级别。如果我们将自定义对象作为参数传递给Logger.log方法,则将从库类的深处调用自定义对象的toString方法。因此,如果我们想要执行繁重的操作以准备对象进行日志记录,我们应该在检查Logger.isLoggable的块内部,或者在对象的toString方法内部执行。

  • 我们不得调用任何对象的toString方法以获取日志消息内容。我们也不得将toString方法调用作为参数传递给Logger.logLogger对象和日志记录框架负责调用自定义对象的toString方法。

  • 必须避免格式字符串连接和日志参数的混合。应用程序用户可能会以错误的意图破坏日志并访问用户未被允许访问的数据,使用恶意连接的字符串是可能的。

Java 标准日志记录的一个主要缺点是性能比较低。标准日志记录所需的时间比其他基于 Java 的日志记录框架(如 Apache Log4j 2、commons logging 或Simple Logging Facade for JavaSLF4J))更长。

Apache Log4j 2

Apache Log4j 是 Java 社区中最广泛使用的日志记录框架之一。它是用 Java 编写的,并在 Apache 软件许可下分发。Apache Log4j 2 是早期版本的修订版。最显著的功能包括线程安全性、性能优化、命名记录器层次结构和国际化支持。

为了设置 Log4j 2,必须在 Maven pom.xml文件中添加以下 Maven 依赖项:

<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-core</artifactId>
  <version>2.7</version>
</dependency>

<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-core</artifactId>
  <version>2.7</version>
  <type>test-jar</type>
  <scope>test</scope>
</dependency>

为了获得测试命名配置文件所需的上下文规则,我们必须在 Maven pom.xml文件中包含test JAR,以及主要的log4j-core包。

Log4j 2 有三个主要的日志记录组件:

  • LoggersLoggers负责捕获日志信息。

  • Appenders这些与 Java 标准日志记录中的Handler对象类似。Appenders负责将日志信息或消息广播到配置的输出介质。

  • LayoutsLayouts负责将日志消息格式化为配置的样式。

以下是log4j2.xml文件的示例:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
  <Appenders>
    <Console name="ConsoleAppender" target="SYSTEM_OUT">
      <PatternLayout pattern="%d [%t] %-5level %logger{36} - 
      %msg%n%throwable" />
    </Console>
  </Appenders>
  <Loggers>
    <Root level="ERROR">
      <AppenderRef ref="ConsoleAppender" />
    </Root>
  </Loggers>
</Configuration>

以下是 Log4j 2 Java 代码示例:

package com.packt.springhighperformance.ch9.logging;

import org.apache.log4j.Logger;

public class SampleLog4j2Example {
  private static Logger logger = 
  Logger.getLogger(SampleLog4j2Example.class);

  public static void main(String argv[]) {
    logger.info("example info log");
    try {
      Bird.fly();
    } catch (Exception ex) {
      logger.error("example error log", ex);
    }
    logger.warn("example warning log");
  }
}

执行上述示例时,将产生以下输出:

2018-02-22 01:18:09 INFO SampleLog4j2Example:9 - example info log
2018-02-22 01:18:09 WARN SampleLog4j2Example:15 - example warning log

Apache Log4j 2 具有超出常见日志级别的额外日志级别。这些是ALLOFF级别。当我们想要启用ALL日志级别时,使用ALL日志级别。如果配置了ALL日志级别,则不考虑级别。OFF日志级别是ALL日志级别的相反。它禁用所有日志记录。

应用程序监控

如前所述,应用程序性能被认为是任何软件应用程序生命周期中最重要的里程碑之一。还需要应用程序能够持续良好地运行。这是我们确保应用程序用户将获得最佳体验的一种方式。这也意味着应用程序正常运行。应用程序性能监控工具跟踪应用程序中进出的每个请求和响应,处理来自请求的信息,并在图形用户界面中响应和显示。这意味着监控工具为管理员提供了快速发现、隔离和解决影响性能的问题所需的数据。

监控工具通常收集有关 CPU 利用率、内存需求、带宽和吞吐量的数据。可以为不同的监控系统设置多个监控系统。任何应用程序性能监控的重要方面之一是将这些监控系统的数据合并到统计分析引擎中,并在仪表板上显示。仪表板使数据日志易于阅读和分析。应用程序监控工具帮助管理员监控应用程序服务器,以便遵守服务级别协议SLA)。设置业务规则以在出现问题时向管理员发送警报。这确保了业务关键功能和应用程序被视为更高优先级。在快速变化的环境中,快速部署在生产系统中变得非常重要。快速部署意味着引入影响系统架构的错误或减慢系统运行的机会更多。

基于这些基本概念,有许多实现和工具可用。应用程序监控工具市场庞大而拥挤,包括行业领先和知名工具,如 AppDynamics、New Relic 和 Dynatrace。除了这些知名工具,还存在开源应用程序监控工具。开源工具包括 Stagemonitor、Pinpoint、MoSKito、Glowroot、Kamon 等。我们将在以下部分详细介绍这些工具。

Stagemonitor

Stagemonitor 具有支持集群应用程序堆栈的监控代理。该工具的目的是监控在多台服务器上运行的应用程序,这是一个常见的生产场景。Stagemonitor 经过优化,可与时间序列数据库集成。它经过优化,用于时间序列数据管理,包括按时间索引的数字数组。这些数据库包括 elasticsearch、graphite 和 InfluxDB。Stagemonitor 也可以在私有网络中设置。它使用开放跟踪 API 来关联分布式系统中的请求。它具有定义指标阈值的功能。Stagemonitor 还支持创建新插件和集成第三方插件。

Stagemonitor 包含一个基于 Java 的代理。代理位于 Java 应用程序中。代理连接到中央数据库,并发送指标、请求跟踪和统计信息。Stagemonitor 需要一个实例来监控所有应用程序、实例和主机。

在浏览器中,在监控端,我们可以看到集群的历史或当前数据。我们还可以创建自定义警报。还可以为每个指标定义阈值。Stagemonitor 有一个仪表板。该仪表板用于可视化和分析不同的感兴趣的指标和请求。Stagemonitor 支持创建自定义仪表板、编写自定义插件和使用第三方插件。它还支持浏览器小部件,而无需后端,并自动注入到受监视的网页中。

以下是 Stagemonitor 仪表板的屏幕截图供参考:

Stagemonitor 仪表板视图(来源:http://www.stagemonitor.org/)

Pinpoint

Pinpoint 与 Stagemonitor 不同之处在于,它是针对大规模应用程序开发的。它是在 Dapper(由 Google 开发的分布式系统跟踪基础设施)之后开发的,旨在为开发人员提供有关复杂分布式系统行为的更多信息。

Pinpoint 有助于分析整个系统结构以及系统不同组件之间的相互关系。Pinpoint 通过跟踪分布式应用程序中的事务来实现这一点。它旨在解释每个事务的执行方式,跟踪组件之间的流动以及潜在的瓶颈和问题区域。

Pinpoint 类似于 Stagemonitor,具有用于可视化的仪表板。该仪表板有助于可视化组件之间的相互关系。该仪表板还允许用户在特定时间点监视应用程序中的活动线程。Pinpoint 具有跟踪请求计数和响应模式的功能。这有助于识别潜在问题。它支持查看关键信息,包括 CPU 利用率、内存利用率、垃圾收集和 JVM 参数。

Pinpoint 由四个组件组成,分别是 Collector、Web、Sample TestApp 和 HBase。我们可以通过分别为每个组件执行脚本来运行一个实例。

以下是参考的 Pinpoint 仪表板:

Pinpoint 仪表板参考视图(来源:http://www.testingtoolsguide.net/tools/pinpoint-apm/)

MoSKito

MoSKito 是三个工具的组合:

  • MoSKito-Essential:这个独立项目是 MoSKito 的核心。它使监视应用程序成为可能。

  • MoSKito-Central:这是一个集中式存储服务器。它存储所有与性能相关的信息。

  • MoSKito-Control:这个工具适用于多节点 Web 应用程序。它提供了对多节点 Web 应用程序的监视支持。

要设置 MoSKito,我们需要在应用程序的WEB-INF/lib目录中安装一个 JAR 文件,这是一个常用的存放 API 库的文件夹。也可以通过在web.xml文件中添加一个新的部分来设置。

该工具能够收集所有应用程序性能指标,包括内存、线程、存储、缓存、注册、付款、转换、SQL、服务、负载分布等等。它不需要用户在应用程序中进行任何代码更改。它支持所有主要的应用服务器,包括 Tomcat、Jetty、JBoss 和 Weblogic。它将数据存储在本地。

MoSKito 还具有通知功能,当达到阈值时会广播警报。它还记录用户的操作,这可能对监视目的有所帮助。MoSKito 提供了一个用于在移动设备上监视应用程序的移动应用程序。它还具有基于 Web 的仪表板。

MoSKito 的一个显著特点是它在 Java 社区中非常稳定和知名。它得到了社区和团队的支持,包括付费支持。

以下是 MoSKito 仪表板的参考:

MoSKito 仪表板视图(来源:https://confluence.opensource.anotheria.net/display/MSK/Javaagent+light+and+multiple+java+processes)

Glowroot

Glowroot 是一种快速、干净、简单的应用程序性能监控工具。它具有一个功能,允许跟踪慢请求和错误。使用 Glowroot,还可以记录每个用户操作所花费的时间。Glowroot 支持 SQL 捕获和聚合。Glowroot 提供的历史数据滚动和保留配置是其提供的附加功能之一。

Glowroot 支持在图表中可视化响应时间的分解和响应时间百分位数。它具有响应灵敏的用户界面,允许用户使用移动设备以及桌面系统监视应用程序,无需进行任何额外的安装。

Glowroot 以 ZIP 文件捆绑提供。要开始使用 Glowroot,我们必须下载并解压 ZIP 文件捆绑。Glowroot 需要更改应用程序的 JVM 参数。我们必须在应用程序的 JVM 参数中添加-javaagent:<path to glowroot.jar>

Glowroot 一旦设置并运行,就提供了带有过滤的持续性能分析。我们还可以设置响应时间百分位数和 MBean 属性的警报。Glowroot 还支持跨多个线程的异步请求。在应用服务器方面,Glowroot 支持 Tomcat、Jetty、JBoss、Wildfly 和 Glassfish。

以下是 Glowroot JVM 仪表板供参考:

Glowroot JVM 仪表板视图(来源:https://demo.glowroot.org

New Relic

New Relic 是 Java 社区中另一个广泛使用的应用程序性能监控工具。New Relic 为应用程序和网络性能统计提供了分组视图。这有助于快速诊断域级问题。它还提供了针对特定请求的深入功能,以查看响应时间、数据传输大小和吞吐量的性能指标。

New Relic 支持使用 Java、Scala、Ruby、Python、PHP、.NET 和 Node.js 开发的应用程序。New Relic 提供了四种不同的后端监控方法:

  • 应用程序性能管理:在应用程序性能管理中,New Relic 提供高级指标,并能够深入到代码级别,以查看应用程序的性能。在仪表板上,New Relic 显示响应时间图表。New Relic 使用 Apdex 指数评分方法将指标转化为性能指标。New Relic 要求用户手动设置阈值。

  • 服务器监控:New Relic 关注应用程序服务器运行的硬件。测量包括 CPU 使用率、内存利用率、磁盘 I/O 和网络 I/O。New Relic 提供了堆内存和垃圾回收属性的简要详情。

  • 数据库监控:在 New Relic 中,数据库仪表板是应用程序性能管理仪表板的一部分。可以通过插件查看数据库监控指标。

  • 洞察和分析:New Relic 具有内置的、可选择的数据库,用于存储统计数据并实现对数据库的查询。

以下是 New Relic 仪表板供参考:

New Relic 仪表板视图(来源:https://newrelic.com/)

性能分析工具

性能分析工具,或者分析器,是应用程序开发人员用来调查和识别代码特征和问题的软件工具。性能分析工具还有助于识别性能问题。性能分析工具回答问题,比如 JVM 参数设置是什么,堆内存的状态如何,基于代的内存利用情况如何,哪些线程是活跃的等等。一些分析器还跟踪代码中的方法,以了解 SQL 语句调用的频率,或者 Web 服务调用的频率。

与应用程序性能监控工具类似,市场上有许多性能分析工具。VisualVM、JConsole 和 HeapAnalyzer 是其中的几个。我们将在接下来的部分详细讨论每个性能分析工具。

VisualVM

VisualVM 是一个 Java 性能分析和性能分析工具。它具有可视化界面,用于分析在本地和远程环境中在 JVM 上运行的 Java 应用程序的详细信息。它集成并利用了 JDK 提供的命令行工具,如jstackjconsolejmapjstatjinfo。这些工具是标准 JDK 分发的一部分。VisualVM 在解决运行时问题方面非常重要,具有堆转储和线程分析等功能。它有助于识别应用程序性能以及其与基准的比较情况。它还有助于确保最佳的内存使用。它进一步有助于监视垃圾收集器,分析 CPU 使用情况,分析堆数据和跟踪内存泄漏。以下是 VisualVM 使用的每个命令行工具的目的:

  • jstack这个工具用于捕获 Java 应用程序的线程转储

  • jmap这个工具打印给定进程的共享对象内存映射和堆内存详细信息

  • jstat这个工具显示运行应用程序的 JVM 的性能统计信息

  • jinfo这个工具打印 Java 配置信息

VisualVM 是标准 JDK 捆绑包的一部分。它首次与 JDK 平台捆绑在 JDK 版本 6,更新 7 中。它也可以单独安装。让我们详细看看每个部分:

VisualVM 的应用程序窗口视图

如前面的屏幕截图所示,在窗口的左侧有一个应用程序窗口。应用程序窗口具有节点和子节点。可以展开节点和子节点以查看配置的应用程序和保存的文件。通过右键单击节点并从弹出菜单中选择项目,可以查看其他信息或执行操作。弹出菜单选项因所选节点而异。

在应用程序窗口内,我们可以看到一个本地节点的菜单。本地节点显示有关在与 VisualVM 相同的计算机上运行的 Java 进程的进程名称和进程标识符的信息。启动 VisualVM 后,当展开本地根节点时,本地节点会自动填充。VisualVM 始终加载为本地节点之一。服务终止时,节点会自动消失。如果我们对应用程序进行线程转储和堆转储,这些将显示为子节点。

可以使用 VisualVM 连接到在远程计算机上运行的 JVM。所有这些运行的进程或应用程序都显示在远程节点下。与远程节点建立连接后,可以展开远程节点以查看在远程计算机上运行的所有 Java 应用程序。

如果应用程序在 Linux 或 Solaris 操作系统上运行,则 VM Coredumps 节点仅可见。在 VisualVM 中打开核心转储文件时,VM Coredumps 节点显示打开的核心转储文件。这是一个包含有关机器运行时状态的二进制文件。

应用程序窗口中的最后一个部分标有快照。快照部分显示在应用程序运行时拍摄的所有保存的快照。

VisualVM 中的本地或远程应用程序的数据以选项卡的形式呈现。在查看应用程序数据时,默认情况下打开概述选项卡。概述选项卡显示的信息包括进程 ID,系统位置,应用程序的主类,Java 安装路径,传递的 JVM 参数,JVM 标志和系统属性。

列表中的下一个选项卡是监视选项卡。监视选项卡可用于查看有关堆内存,永久代堆内存以及类和线程数量的实时信息。这里的类表示加载到虚拟机中的类。应用程序监视过程的开销较低。

监视选项卡上的堆图显示了总堆大小和当前使用的堆大小。在 PermGen 图中显示了永久代区域随时间的变化。类图显示了加载和共享类的总数。线程部分显示了活动线程和守护线程的数量信息。VisualVM 可以用于获取线程转储,显示特定时间的线程的确切信息。

在监视选项卡中,我们可以强制执行垃圾回收。该操作将立即运行垃圾回收。还可以从监视选项卡中捕获堆转储:

VisualVM 在线程选项卡中显示实时线程活动。默认情况下,线程选项卡显示当前线程活动的时间轴。通过单击特定线程,可以在详细信息选项卡中查看有关该特定线程的详细信息。

时间轴部分显示了带有实时线程状态的时间轴。我们可以通过选择下拉菜单中的适当值来过滤显示的线程类型。在上述屏幕截图中,显示了活动线程的时间轴。我们还可以通过从下拉菜单中选择来查看所有线程或已完成线程。

在应用程序运行时,我们可以选择获取应用程序的线程转储。打印线程转储时,会显示包括 Java 应用程序的线程状态的线程堆栈。

分析器选项卡使得可以启动和停止应用程序的分析会话。结果显示在分析器选项卡中。可以进行 CPU 分析或内存分析。启动分析会话后,VisualVM 连接到应用程序开始收集分析数据。一旦结果可用,它们将自动显示在分析器选项卡中。

JConsole

JConsole 是另一个 Java 分析工具。它符合Java 管理扩展JMX)规范。JConsole 广泛使用 JVM 中的仪器来收集和显示运行在 Java 平台上的应用程序的性能和资源消耗的信息。JConsole 在 Java SE 6 中更新为 GNOME 和 Windows 外观。

与 VisualVM 类似,JConsole 与 Java 开发工具包捆绑在一起。JConsole 的可执行文件可以在JDK_HOME/bin目录中找到。可以使用以下命令从命令提示符或控制台窗口启动 JConsole:

jconsole

执行上述命令后,JConsole 会向用户显示系统上运行的所有 Java 应用程序的选择。我们可以选择连接到任何正在运行的应用程序。

如果我们知道要连接到的 Java 应用程序的进程 ID,也可以提供进程 ID。以下是启动 JConsole 并连接到特定 Java 应用程序的命令:

jconsole <process-id>

可以使用以下命令连接到在远程计算机上运行的 Java 应用程序:

jconsole hostname:portnumber

JConsole 在以下选项卡中显示信息:

  • 概述:此选项卡显示有关 JVM 和要监视的值的信息。它以图形监视格式呈现信息。信息包括有关 CPU 使用情况、内存使用情况、线程计数以及 JVM 中加载的类数量的概述细节。

  • 内存:此选项卡显示有关内存消耗和使用情况的信息。内存选项卡包含一个执行 GC 按钮,可以单击以立即启动垃圾回收。对于 HotSpot Java VM,内存池包括伊甸园空间、幸存者空间、老年代、永久代和代码缓存。可以显示各种图表来描述内存池的消耗情况。

  • 线程:此选项卡显示有关线程使用情况的信息。线程包括活动线程、活动线程和所有线程。图表的表示显示了线程的峰值数量和两条不同线上的活动线程数量。MXBean 提供了线程选项卡未涵盖的其他信息。使用 MXBean,可以检测到死锁线程。

  • 类:此选项卡显示了 Java 虚拟机中加载的类的信息。类信息包括迄今为止加载的类的总数,包括后来卸载的类以及当前加载的类的数量。

  • VM:此选项卡显示有关 Java 虚拟机的统计信息。摘要包括正常运行时间,表示 JVM 启动以来的时间量;进程 CPU 时间,表示 JVM 自启动以来消耗的 CPU 时间量;以及总编译时间,表示用于编译过程的时间。

  • MBeans:此选项卡显示有关 MBeans 的信息。MBeans 包括当前正在运行的 MBeans。我们可以通过选择 MBean 来获取MBeanInfo描述符信息。

以下截图显示了 JConsole 仪表板的外观:

总结

本章充满了有关应用程序性能测量技术的信息。本章对于致力于应用程序性能增强任务的开发团队非常有用。同时,技术团队在设置其应用程序日志记录机制时也可以参考本章。

本章从性能分析和日志记录的简介细节开始。继续前进,我们了解了特定应用程序性能监控和应用程序日志记录。我们了解了日志记录的关键要素是什么。我们还研究了日志记录工具,如标准 Java 日志记录和 Log4j。在本章的后半部分,我们了解了 VisualVM 作为性能分析工具。VisualVM 是最广泛使用的基于 Java 的性能分析工具之一,作为标准 Java 分发包提供。就是这样了。

下一章将重点关注优化应用程序性能。在进行性能优化时,可以利用本章提供的知识和信息。本章为下一章提供了基础。下一章涵盖了识别性能问题症状、性能调优生命周期和 Spring 中的 JMX 支持的详细信息。非常令人兴奋,不是吗?

第十章:应用性能优化

在上一章中,我们重点介绍了如何对应用程序进行分析以找出应用程序的性能问题。我们还涵盖了日志记录,这是识别应用程序问题的有用工具。这是一个重要的部分,并且在我们处理 Spring 应用程序时将成为我们日常工作的一部分。

现在让我们看看本章内容。这是本书中的一个关键章节;它为您提供了改善应用性能的方法。在本章中,我们将讨论应用性能优化的基本方法,这对于任何应用程序都是关键的,包括基于 Spring 的应用程序。我们将讨论 Spring 对 Java 管理扩展(JMX)的支持,数据库交互的改进以及 Spring 应用程序的性能调优。通过本章结束时,您将能够识别 Spring 应用程序中的性能瓶颈并解决它们。

让我们以结构化的方式来看应用性能优化的重要方面。我们将涵盖以下主题:

  • 性能问题症状

  • 性能调优生命周期

  • 性能调优模式和反模式

  • 迭代性能调优过程

  • Spring 对 JMX 的支持

性能问题症状

让我们从性能问题症状开始。这是一个明显的起点,就像咨询医生一样,首先讨论症状,然后做出诊断。应用性能是用户在速度、交付内容的准确性和最高负载下的平均响应时间方面所经历的行为。负载是指应用程序每单位时间处理的交易数量。响应时间是应用程序在这样的负载下响应用户操作所需的时间。

每当性能需要改进时,首先想到的是影响我们应用程序性能的问题。要找出性能问题,我们需要寻找一些症状,这些症状可以引导我们找到问题。

在 Spring 应用中可能观察到的一些常见症状如下:

  • 超时

  • 工作线程不足

  • 线程等待类加载器

  • 即使在正常负载下,加载类所花费的大量时间

  • 类加载器尝试加载不存在的类

在接下来的章节中,我们将通过一个示例情境来理解这些症状。这些细节将帮助我们在发生时识别症状。

超时

超时以两种不同的方式发生。一种是请求超时,由 HTTP 响应状态码 408 表示。另一种超时是网关超时,由 HTTP 响应状态码 504 表示。

请求超时表示服务器未能在指定时间内从客户端接收完整的请求。在这种情况下,服务器选择与客户端关闭连接。请求超时是服务器直接的错误消息。

网关超时表示网关或代理服务器在处理请求时超时。在大多数情况下,这是因为代理或网关未能及时从上游的实际服务器接收到响应。

工作线程不足

以银行为例;银行拥有一个带有监控系统的 Web 应用程序。监控系统关注 JVM 的强度。测量参数包括内存、CPU、I/O、堆内存和其他各种属性。监控系统提供了独特的仪表板,显示并突出显示了上述属性的测量结果。还有一个附带的仪表板,显示了银行应用程序中执行的活动组。该仪表板还确定了 JVM 在访问专门的应用程序资源(如线程)时开始运行低的活动组。该应用程序在多个 JVM 环境中运行。以下是一个示例仪表板的屏幕截图,仅供参考:

监控系统配置了阈值。例如,JVM 一次使用的最大线程数不应超过 250 个。当 JVM 一次使用的线程少于 150 个时,仪表板中相应的 JVM 指示灯为绿色。当 JVM 开始使用超过 150 个线程时,监控系统会将该 JVM 指示为红色。这是一个症状,表明可能会发生故障或性能受到异常影响。

以下是一个基于时间线的屏幕截图,显示了 JVM 的工作线程达到最大值:

线程在类加载器上等待

继续使用前一节中描述的相同示例,首先出现的问题是,这些线程有什么问题?深入研究线程并分解状态后发现,这些线程(大约 250 个中的 242 个)正在寻找服务器的CompoundClassLoader。这些线程正在堆叠额外的对象,这就是它们正在寻找类加载器的原因。由于大量线程试图访问这个共享资源——类加载器,大多数线程都陷入了暂停状态。

监控显示了等待CompoundClassLoader的线程数量:

在类加载活动上花费的时间

在监控系统中进行的分析还表明,线程大部分时间都花在类加载活动上。以下是突出显示这一点的监控系统截图:

从监控系统的先前屏幕截图来看,很明显,无论当前负载如何,与请求处理生命周期中的其他活动相比,类加载活动都需要相当长的时间。这是性能问题的指标或症状,因为它会增加整体响应时间。在银行的情况下,可以通过评估平均响应时间来确认。

类加载器尝试加载不存在的类

一个问题出现了:类堆叠是否非常重要?深入挖掘并查看处理的请求,表明每个请求都试图堆叠一个不存在的类。应用服务器正在提示大量的ClassNotFoundException类。问题的主要驱动因素是该类无法被有效地堆叠,但应用服务器继续尝试为每个请求堆叠它。这对于快速和中等请求和功能来说不应该是一个问题。对于每个传入请求或功能的这种细节水平可能会占用稀缺资源——类加载器,并因此影响请求的响应时间。

监控系统的能力、适应性和容量是捕捉每个请求和响应以及有关堆叠类数据的关键。以下屏幕截图显示了应用框架中的一个这样的场景:

现在应该清楚了潜在性能问题的症状。这特别适用于任何基于 JVM 的 Web 应用程序,而不仅仅是基于 Spring 的 Web 应用程序。以下截图显示了基本上可以帮助我们识别性能问题影响的指针。

性能不佳的应用对企业非常重要,因为它们因应用性能而导致销售额下降。应用也可能因性能问题而导致生产率或业务损失。

让我们通过一个基本示例来了解性能问题对业务的影响:

从上图可以理解,糟糕的应用行为会影响业务,可能导致项目成本高、转化率下降、重复访问减少、客户保留率低、销售额下降、生产率下降、客户流失、项目成本增加,以及利润和投资回报的延迟或下降。性能对企业非常重要。

我们需要做什么来避免或解决性能问题?不要等待性能问题发生。提前进行架构、设计和代码审查,并计划进行负载测试、调优和基准测试。如今,在竞争激烈的市场中,组织的关键是确保其系统以最佳性能运行。任何故障或停机都直接影响业务和收入;应用程序的性能是一个不容忽视的因素。由于技术的广泛应用,数据量日益增长。因此,负载平均值正在飙升。在某些情况下,无法保证数据不会超出限制或用户数量不会超出范围。

在任何时候,我们都可能遇到意想不到的扩展需求。对于任何组织来说,其应用程序提供可伸缩性、性能、可用性和安全性非常重要。在多个服务器上分布数据库以满足不同应用程序查询的应用程序可伸缩性,无论是水平扩展还是垂直扩展,都是相当可行的。向集群添加计算能力以处理负载非常容易。集群服务器可以立即处理故障并管理故障转移部分,以保持系统几乎一直可用。如果一个服务器宕机,它将重定向用户的请求到另一个节点并执行所请求的操作。如今,在竞争激烈的市场中,组织的关键是确保其系统正常运行。任何故障或停机都直接影响业务和收入;高可用性是一个不容忽视的因素。

以下图表显示了我们可能遇到的一些常见性能问题:

现在,让我们来看看性能调优生命周期的各个阶段。

性能调优生命周期

速度是每个企业的核心。在这个超连接的现代世界中,大多数人着迷的是速度;无论是最快的汽车、最快的计算机处理器,甚至是最快的网站。网站性能已经成为每个企业的最高优先级。用户的期望比以往任何时候都更高。如果您的网站不能立即响应,很有可能用户会转向竞争对手。

沃尔玛的一项研究发现,每提高 1 秒的页面性能,转化率就会增加 2%。

Akamai 的一项研究发现:

  • 47%的人期望网页在两秒内加载完成

  • 如果一个网页加载时间超过三秒,40%的人会放弃访问

  • 52%的在线购物者表示快速页面加载对他们对网站的忠诚度很重要

2007 年,亚马逊报告称,亚马逊(www.amazon.com/)的加载时间每增加 100 毫秒,销售额就会减少 1%。

借助以下图,我们可以轻松理解性能调优生命周期的不同阶段:

在大多数情况下,通过在适当的时候审查以下工件,可以避免性能问题:

  • 架构

  • 设计

  • 代码

  • 聘请专家顾问在适当的时候进行应用程序审查

  • 在开发阶段完成之前的任何时间进行审查

  • 强烈建议提前识别性能优化问题,这可以在架构阶段完成之前开始

  • 在向用户提供应用程序之前,最好预防性能问题

  • 进行各种审查和测试,以避免生产中的性能问题

  • 性能调优生命周期也可以在投入生产后或在生产环境中面临性能问题时进行

为了调整 Spring 应用程序的性能,以下部分描述的策略可能非常有用。

连接池

连接池是一种帮助应用程序执行的策略,其中打开和管理数据库的N个连接在池中。应用程序只需请求连接,使用它,然后将其放回池中。当应用程序请求连接时,准备好的连接保持可用以供池中使用。池管理连接的生命周期,以至于开发人员实际上不必等待连接并筛选过时的连接。

Hibernate 利用其魔力来识别要使用的连接池提供程序 - 基于您配置的属性。

以下是 c3p0 连接池的属性配置:

<property name="hibernate.c3p0.min_size">5</property>
<property name="hibernate.c3p0.max_size">20</property>
<property name="hibernate.c3p0.timeout">300</property>
<property name="hibernate.c3p0.max_statements">50</property>
<property name="hibernate.c3p0.idle_test_period">3000</property>

以下是 Apache Commons DBCP 的连接池属性配置示例:

<property name="hibernate.dbcp.initialSize">8</property>
<property name="hibernate.dbcp.maxActive">20</property>
<property name="hibernate.dbcp.maxIdle">20</property>
<property name="hibernate.dbcp.minIdle">0</property>

在使用任何连接池机制时,我们必须手动将 JAR 依赖项放置在服务器类路径中,或者使用 Maven 等依赖管理工具。

还可以使用hibernate.connection.provider_class属性明确指定连接提供程序,尽管这不是强制性的。

如果我们不使用 Hibernate 配置连接池,默认会使用。当启动应用程序时,可以在日志或控制台输出中看到:

org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure

Hibernate 的默认连接池对于开发环境是一个不错的选择,但是在生产环境中,建议根据要求和用例配置连接池。

如果您使用应用程序服务器,可能希望使用内置池(通常使用Java 命名和目录接口JNDI)获取连接)。

要使用服务器的内置池与使用 JNDI 配置的 Hibernate 会话,我们需要在 Hibernate 配置文件中设置以下属性:

hibernate.connection.datasource=java:/comp/env/jdbc/AB_DB

假设AB_DB是 Tomcat JDBC 连接池DataSource的 JNDI 名称。

如果您不能或不希望使用应用程序服务器内置的连接池,Hibernate 支持其他几种连接池,例如:

  • c3p0

  • Proxool

在 Apache DBCP 之后,第二受欢迎的连接池实现是 c3p0,它可以轻松集成 Hibernate,并且据说性能良好。

Hibernate

连接池机制确保应用程序在需要时不会耗尽数据库连接。Hibernate 是 Java 应用程序的最佳 ORM 框架之一。在使用时,必须进行性能优化。

事务

Hibernate 只在需要时进行脏检查,以考虑执行成本。当特定实体具有与大量列对应的表时,成本会增加。为了尽量减少脏检查成本,最好我们通过指定一个交易来帮助 Spring 进行读取,这将进一步提高执行效率,消除了任何脏检查的需求。

以下是@Transactional注解的一个示例用法,该注解表示该方法在 Hibernate 事务中运行:

@Transactional(readOnly=true)
public void someBusinessMethod() {
    ....
}

定期清除 Hibernate 会话

在数据库中包含/调整信息时,Hibernate 会维护会话。在会话中,它存储了将要保存的实例的形式。如果在会话关闭之前修改了这些实例或记录,这就被称为脏检查。然而,我们可以让 Hibernate 不要在其会话中保存元素的时间比实际需要的时间长。因此,一旦需求完成,我们就不必再在会话中保存实例。在这种情况下,我们可以安全地刷新和清除EntityManager,以调整数据库中元素的状态并将实例从会话中移除。这将使应用程序远离内存需求,并且肯定会对执行产生积极影响。

以下是一段代码,可以用来flush()clear() Hibernate 会话:

entityManager.flush();
entityManager.clear();

懒加载

如果您正在使用 Hibernate,您应该注意适当使用IN语句。它只在需要时才懒惰加载记录。当这些自定义记录被不高效地加载到内存中时,每个记录将被单独加载并单独使用。因此,如果内存中加载了太多实例,那么将依次执行许多查询,这可能导致严重的执行问题。

基于构造函数的 HQLs

在正常情况下,当应用程序使用 Hibernate 时,我们不会尝试检索整个内容及其所有属性,尽管我们不需要它们用于特定用例。一个实体可能有 30 个属性,而我们可能只需要其中几个在我们的功能中设置或显示给用户。在这种情况下,将使用查询从数据库中检索大量记录。考虑到未使用的字段与应用程序粘合在一起,这将最终导致巨大的执行或性能问题。

为了解决这个问题,HQL/JPA 为我们提供了一个 select new 构造函数调用,通常用于制定查询,这也使开发人员能够选择聚合值。

实体和查询缓存

如果每次为特定实体调用相同的查询,并且表数据对于特定可用性不会发生变化,我们可以使用 Hibernate 存储查询和实体。

如果应用了查询缓存,那么对于执行,后续的 SQL 语句将不会发送到数据库。如果查询缓存或一级缓存找不到基于标识符的元素,那么将使用存储的元素标识符来访问 Hibernate 的二级缓存,其中存储了相应的实际元素。这对响应时间有很大影响。当我们这样做时,我们也关心缓存何时刷新自身。我们可以通过一些简单的设置轻松实现这一点。

本地查询

尽管本地查询有缺点,但在执行方面它们是最快的。当 HQL 更改无法改善应用程序的执行时,本地查询可以显著提高执行效率,大约提高 40%。

主键生成

在将 Hibernate 注释指示到实体类或编写.hbm文件时,我们应该避免使用自动键生成方法,这会导致大量的序列调用。

以下是定义密钥生成策略的示例代码:

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "your_key_generator")
private Long id;

通过这个简单的改变,在插入密集型应用程序中可以注意到 10-20%的改进,基本上不需要代码更改。

数据库

一旦完成了 Hibernate 性能优化生命周期,下一步将是在数据库级别执行优化生命周期。以下部分定义了数据库组件的性能改进技术。

索引

如果查询涉及的表具有大量列,则列表成为一个重要因素。此外,当复杂的数据库查询被应用程序终止时,它也会产生影响。获取所需索引建议的最佳方法是检查查询执行计划。在分析用于索引的 SQL 查询时,我们必须分别预期每一个真实的查询。

在使用索引时,必须注意以下几点:

  • 索引可能会减慢插入和更新,因此在经常更新的列上谨慎应用它们

  • 索引旨在加速使用查询中的WHEREORDER BY子句的搜索操作

视图

数据库视图是我们在高度围绕较长的执行时间问题时探索或考虑的另一种过程。直到 SQL Server 2000,视图仅用于便利,而不是速度。SQL Server 的后续版本包括一个称为索引视图的特殊功能,据说可以大大提高性能,但必须使用一套规则创建索引视图。

Spring Security

Spring Security 对于任何应用程序都是最重要的方面之一,特别是那些在互联网上运行的应用程序。虽然 Spring Security 为应用程序提供了安全外观并防止应用程序被非法访问,但如果管理不当,它会增加很多额外负担。我们将在接下来的部分重点介绍 Spring Security 的最佳实践。

认证缓存

Spring Security 执行是偶尔出现的担忧之一,当需求处理时间被认为过高,因此不可接受。有时你会发现真正的需求处理大约需要 120 毫秒,而 Spring Security 验证/验证又需要另外 500-600 毫秒。

LDAP 自定义权限

这可能不是你需要考虑的方法,但它确实为你提供了另一种增强 Spring Security 实现执行的选择。

在这种方法中,我们使用自己的自定义方式设置用户权限,而不是从 LDAP 进行确认。这样做有几个很好的理由,应用程序的执行是其中之一。

本地 LDAP

Spring Security 为我们提供了最标准和可靠的 LDAP 验证实现。通过中心 Spring LDAP,方法变得有点糟糕,但显示出了优化改进的迹象。最后一种方法(使用中心 Spring LDAP)被看到与 Spring Security 相比,极大地提高了应用程序的执行。这不是首选的方法,但我们可以考虑它作为发展的选择之一。

多线程

现在每个应用程序都是多线程的,这意味着它能够同时执行多个操作。

对于每一个可能的优化,对应用程序的单次点击可能看起来令人满意。然而,在应用程序遭受多个同时点击的负载测试时,应用程序的执行开始受到阻碍。在这种高度并发的情况下,您可能需要调整 Tomcat 服务器上的线程默认设置。如果存在高度并发性,HTTP 请求将被挂起,直到线程处理它。在更极端的情况下,等待队列会升高,请求会超时。

默认服务器线程使用可以通过在业务逻辑内部使用代理结构来进一步补充,以便在单个线程执行流中进一步进行并发非同步调用。

性能调优模式和反模式

性能调优是改变系统执行情况。通常在计算机系统中,这样做的动机被称为性能问题,可以是真实的或假设的。大多数系统会对增加的负载做出一定程度的执行降低。系统接受更高负载的能力称为可扩展性,调整系统以处理更高负载等同于性能调优。

性能调优包括以下步骤:

  1. 问题应该根据预期的数字计数进行评估和检查以满足要求。

  2. 修改前测量系统的执行情况。

  3. 识别系统中关键的部分以改善执行情况。这被称为瓶颈

  4. 修改系统的部分以消除瓶颈。

  5. 修改后测量框架的执行情况。

  6. 如果调整改善了执行情况,请接受它。如果改变恶化了执行情况,请将其恢复到原样。

反模式

与模式一样,软件开发中也存在反模式。模式有助于确保应用程序在性能、可扩展性和优化处理方面的改进。另一方面,代码中存在反模式表明应用程序执行存在挑战。反模式以与模式类似的程度影响应用程序,但是以负面方式。性能反模式大多会降低应用程序的性能。我们讨论反模式,是因为除了遵循模式和最佳实践外,我们还必须确保不遵循或使用反模式。

架构反模式

架构中存在许多类型的性能反模式。多层反模式描述了一种试图通过尽可能多的独立的逻辑应用层来实现高抽象的架构。作为开发人员,这样的架构很快就会因为大部分时间花在映射和转换数据上而变得可识别,并且从界面到数据库的简单传递变得复杂。

这种架构通常出现是因为应用程序应该尽可能灵活,以便可以轻松快速地交换 GUI,并且对其他系统和组件的依赖性可以保持较低。层的解耦导致在映射和数据交换过程中出现性能损失,特别是如果层也是物理上分离的,并且数据交换通过远程技术进行,例如简单对象访问协议SOAP)或远程方法调用RMI),Internet 对象请求代理协议IIOP)。

许多映射和转换操作也可能导致更高的垃圾收集活动,这被称为循环对象问题。作为解决这种反模式的方法,应该仔细审查架构驱动程序,澄清需要什么灵活性和解耦。新的框架方法,如 JBoss Seam,已经解决了这个问题,并尽量避免映射数据。

另一个架构反模式是所谓的会话缓存。这样做,应用程序的 Web 会话被误用为大型数据缓存,严重限制了应用程序的可扩展性。调整工作中经常测量到会话大小远远大于 1MB,在大多数情况下,没有团队成员知道会话的确切内容。大型会话会导致 Java 堆非常繁忙,只能容纳少量并行用户。特别是在使用会话复制进行集群应用时,根据所使用的技术,由于序列化和数据传输而导致的性能损失非常严重。一些项目帮助获取新的硬件和更多内存,但从长远来看,这是一个非常昂贵和风险的解决方案。

会话缓存的产生是因为应用程序的架构没有清楚地定义哪些数据是会话相关的,哪些是持久的,即随时可恢复的。在开发过程中,所有数据都很快地存储在会话中,因为这是一个非常方便的解决方案——通常这些数据不再从会话中删除。要解决这个问题,首先应该使用生产堆转储对会话进行内存分析,并清理不是会话相关的数据。如果获取数据的过程对性能至关重要,例如数据库访问,缓存可以对性能产生积极影响。在最佳情况下,缓存对开发人员来说应该是透明的,嵌入在框架中。例如,Hibernate 提供了一级和二级缓存来优化对数据的访问,但要小心;这些框架的配置和调优应该由专家来完成,否则你很快就会遇到新的性能反模式。

实施反模式

有许多 Java 性能反模式和调优技巧可用,但这些技术反模式的问题在于它们严重依赖于 Java 版本和制造商,特别是用例。一个非常常见的反模式是被低估的前端。对于 Web 应用程序,前端通常是性能的软肋。HTML 和 JavaScript 开发经常让真正的应用程序开发人员感到困扰,因此通常对性能进行了低优化。即使在越来越多地使用 DSL 的情况下,连接通常仍然是一个瓶颈,特别是如果是通过通用移动通信系统UMTS)或通用分组无线业务GPRS)的移动连接。Web 应用程序变得越来越复杂,受到 Web 2.0 炒作的推动,并且越来越接近桌面应用程序。

这种便利导致了延长的等待时间和通过许多服务器往返和大页面增加了更高的服务器和网络负载。有一整套解决方案来优化基于 Web 的界面。使用 GZip 压缩 HTML 页面可以显著减少传输的数据量,并且自 HTTP 1.1 以来所有浏览器都支持。例如,Apache 等 Web 服务器有模块(mod_gzip)可以在不改变应用程序的情况下执行压缩。然而,通过一致使用 CSS 并将 CSS 和 JavaScript 源代码交换到自己的文件中,可以快速减小 HTML 页面的大小,以便浏览器更好地缓存。此外,如果正确使用,AJAX 可以显著提高性能,因为可以节省完全重新加载网页的过程;例如,只重新传输列表的内容。

但即使在分析中,通过将页面内容调整到用户的要求,页面的性能也可以得到显着改善。例如,如果页面上只显示 80% 的时间需要的字段,平均传输速率可以显著降低;被删除的字段被卸载到单独的页面上。例如,在许多 Web 应用程序中,有超过 30 个输入字段的表单。在用户填写这些表单的 90% 的情况下,他们只为两个字段填写值,但我们在列表页面或报告中显示了所有这些 30 个字段,包括选择框的所有列表。另一个常见的反模式是幻影日志,几乎所有项目中都可以找到。幻影日志生成实际上不必在活动日志级别中创建的日志消息。以下代码是问题的一个例子:

logger.debug ("one log message" + param_1 + "text" + param_2);

尽管消息不会在INFO级别中记录,但字符串被组装。根据调试和跟踪消息的数量和复杂性,这可能导致巨大的性能损失,特别是如果对象具有重写和昂贵的toString()方法。解决方案很简单:

if (logger.isDebugEnabled ()) {
  logger.debug ("One log message" + param_1 + "Text" + param_2);
}

在这种情况下,首先查询日志级别,只有在DEBUG日志级别处于活动状态时才生成日志消息。为了避免在开发过程中出现性能瓶颈,特别应正确理解所使用的框架。大多数商业和开源解决方案都有足够的性能文档,并且应定期咨询专家以实施解决方案。即使在分析中发现了框架中的瓶颈,也并不意味着问题出现在框架内。在大多数情况下,问题是误用或配置。

迭代性能调优过程

迭代性能调优过程是一组指南,将帮助大幅提高应用程序的性能。这些指南可以在迭代中应用,直到达到期望的输出。这些指南也可以应用于各种 Web 应用程序,无论使用何种技术构建应用程序。

任何应用程序的第一个和最重要的部分是静态内容的渲染。静态内容的传递是最常见的性能瓶颈之一。静态内容包括图像、标志、浏览器可执行脚本、级联样式表和主题。由于这些内容始终保持不变,因此无需动态提供这些内容。相反,应该配置 Web 服务器(如 Apache)在向响应提供静态资源时具有长时间的浏览器缓存时间。静态内容传递的改进可以显著提高应用程序的整体性能。Web 服务器还必须配置为压缩静态资源。可以使用 Web 加速器来缓存 Web 资源。对于内容驱动的公共门户,强烈建议通过 Web 加速器缓存整个页面。Varnish 是一种开源的 Web 加速器工具。

服务器资源监控必须作为迭代性能分析的一部分。原因是随着应用程序的增长,它开始在特定时间占用更多资源。对服务器资源的更高需求,如 CPU、内存和磁盘 I/O,可能导致超出操作系统限制并容易发生故障。监控系统必须设置以观察资源利用情况。资源监控通常包括:

  • Web 服务器

  • 应用服务器

  • 进程-最大与实际

  • 线程-最大与实际

  • 内存使用

  • CPU 利用率

  • 堆内存作为单独的测量

  • 磁盘 I/O 操作

  • 数据库连接-最大与繁忙

  • JVM 垃圾回收

  • 数据库慢查询

  • 缓存

  • 缓存命中-从缓存中找到结果的次数

  • 缓存未命中-未从缓存中找到结果的次数

为了监视资源,可以使用以下工具:

  • jconsolejvisualvm与标准的Java 开发工具包JDK)捆绑在一起。使用这些工具,我们可以监视 JVM、垃圾收集执行、缓存统计、线程、CPU 使用率、内存使用率和数据库连接池统计。

  • mpstatvmstat在基于 Linux 的操作系统上可用。这两者都是命令行工具,用于收集和报告与处理器相关的统计信息。

  • ifstatiostat对于监视系统的输入/输出操作很有用。

可能会有一个问题,为什么我们要在遵循最佳实践的同时进行这个迭代的性能调优过程。迭代性能调优过程的目标如下:

  • 在各个级别识别系统性能瓶颈

  • 根据期望改善门户的性能

  • 找到解决方案和方法

  • 将解决方案工作流程放在适当的位置

  • 了解系统性能的痛点

  • 为应用程序定义性能策略

  • 根据技术选择性能测量工具

  • 了解应用程序的关键用户场景

  • 记录关键场景

  • 准备足够的数据,在单次执行中对所有风味产生可观的分布式负载

  • 定制和组合负载测试脚本,以准备可用于任何单个风味或同时用于所有风味的性能测试套件

  • 使用不同场景和负载组合执行性能脚本,以识别响应时间的瓶颈

迭代性能调优过程在应用程序开发生命周期的所有阶段都得到遵循。以下表格演示了正在审查的项目以及输入和输出期望:

审查项目 输入 输出

| 架构

  • 高可用性

  • 可扩展性

  • 缓存

  • 集成

  • 网络

  • 搜索引擎

  • 数据库

系统架构图 最佳实践建议
用户界面
  • 前端代码

  • 现有技术选择标准

|

  • 代码审查

  • 更改建议

|

硬件配置
  • Web 服务器细节

  • 应用服务器细节

  • 数据库细节

  • 服务器类型(虚拟或物理)

  • CPU 数量

  • 硬盘空间

  • 内存配置

建议在硬件配置中进行的更改
软件配置
  • 框架配置

  • 依赖模块/集成配置,如果有的话

配置更改建议
应用服务器配置
  • 应用服务器配置文件
配置更改建议
Web 服务器配置
  • Web 服务器配置文件

  • 缓存控制设置

  • 静态资源处理设置

配置更改建议
部署架构
  • 部署图

  • 软件安装细节

  • 配置细节

部署架构更改建议
代码和数据库
  • 代码审查

  • 数据库设计审查

  • 代码重复

  • 代码模块化

  • 任何第三方库/ API

  • 实施的编码标准

  • 循环和条件

  • 数据规范化

  • 索引

  • 长时间运行的查询

  • 表之间的关系

|

  • 代码审查结果

  • 改进建议

|

Spring 对 JMX 的支持

JMX 是 Java 平台中的标准组件。它首次发布于 J2SE 5.0。基本上,JMX 是为应用程序和网络管理定义的一组规范。它使开发人员能够为应用程序中使用的 Java 对象分配管理属性。通过分配管理属性,它使 Java 对象能够与正在使用的网络管理软件一起工作。它为开发人员提供了一种标准的方式来管理应用程序、设备和服务。

JMX 具有三层架构。这三层在这里定义:

  • 探针或仪表层:此层包含托管的 bean。要管理的应用程序资源已启用 JMX。

  • 代理或 MBeanServer 层:这一层构成了 JMX 的核心。它作为托管 bean 和应用程序之间的中介。

  • 远程管理层:此层使远程应用程序能够使用连接器或适配器连接到和访问MBeanServer。连接器提供对mBeanServer的完全访问权限,而适配器则适应 API。

以下图表显示了 JMX 的架构:

来源:https://www.ibm.com/support/knowledgecenter/en/SSAW57_8.5.5/com.ibm.websphere.nd.multiplatform.doc/ae/cxml_javamanagementx.html

托管 bean

托管 bean 是一种 Java bean。它专门用于 JMX 技术,并且是使用依赖注入DI)技术创建的。在 JMX 中,资源被表示为托管 beanMBean)。这些托管 bean 被注册到核心托管 bean 服务器中。因此,托管 bean 可以被视为 Java 服务、组件或设备的包装器。由于所有托管组件都注册到 MBeans 服务器,因此它用于管理所有托管 bean。托管 bean 服务器允许服务器组件连接并找到托管 bean。典型的 JMX 代理由托管 bean 服务器和与托管 bean 交互所需的服务组成。

JMX 规范描述了标准连接器。这些连接器也被称为JMX 连接器。JMX 连接器允许我们从远程管理应用程序访问 JMX 代理。连接器可以使用不同的协议与相同的管理接口一起工作。

以下是为什么应该使用 JMX 的原因:

  • 它提供了一种在不同设备上管理应用程序的方法

  • 它提供了一种标准的管理 Java 应用程序和网络的方法

  • 它可以用来管理 JVM

  • 它提供了一个可扩展和动态的管理接口

通过对 JMX 的基本理解,让我们继续检查它在 Spring 中的支持。Spring 对 JMX 的支持使我们能够很容易地将 Spring 应用程序转换为 JMX 架构。

Spring 的 JMX 支持提供的功能如下:

  • 自动将 Spring bean 注册为托管 bean

  • 用于控制 Spring beans 的管理接口的灵活结构

  • 远程连接器上托管 bean 的声明性方法

  • 本地和远程托管 bean 资源的代理

这些功能可以在不与 Spring 或 JMX 的类或接口耦合的情况下工作。Spring JMX 支持有一个名为MBeanExporter的类。这个类负责收集 Spring beans 并将它们注册到托管的 beans 服务器中。

以下是 Spring bean 的示例:

package com.springhighperformance.jmx;

public class Calculator {
  private String name;
  private int lastCalculation;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public int getLastCalculation() {
    return lastCalculation;
  }

  public void calculate(int x, int y) {
    lastCalculation = x + y;
  }
}

为了将此 bean 及其属性公开为托管属性和操作,应在配置文件中进行以下配置:

<beans>
  <bean id="exporter"     
    class="org.springframework.jmx.export.MBeanExporter
    " lazy-init="false">
    <property name="beans">
      <map>
        <entry key="bean:name=calculatorBean1" value-
        ref="calculatorBean"/>
      </map>
    </property>
  </bean>

  <bean id="calculatorBean" 
    class="com.springhighperformance.jmx.Calculator">
    <property name="name" value="Test"/>
    <property name="lastCalculation" value="10"/>
  </bean>
</beans>

从前面的配置中,要查找的一个重要的 bean 定义是导出器 bean。导出器 bean 的 beans map 属性指示要将哪些 Spring beans 暴露为 JMX beans 到 JMX 托管的 beans 服务器。

通过上述配置,假设托管 bean 服务器必须在 Spring 应用程序可访问的环境中运行。如果托管 bean 服务器或MBeanServer正在运行,Spring 将尝试找到它并注册所有 bean。当应用程序在 Tomcat 或 IBM WebSphere 中运行时,这种默认行为是有用的,因为它有捆绑的MBeanServer

在其他情况下,我们必须创建一个MBeanServer实例,如下所示:

<beans>
  <bean id="mbeanServer" class="org.springframework.jmx.support.
    MBeanServerFactoryBean"/>

  <bean id="exporter" 
   class="org.springframework.jmx.export.MBeanExporter">
    <property name="beans">
      <map>
        <entry key="bean:name=calculatorBean1" value-
         ref="calculatorBean"/>
      </map>
    </property>
    <property name="server" ref="mbeanServer"/>
  </bean>

  <bean id="calculatorBean" 
   class="com.springhighperformance.jmx.Calculator">
    <property name="name" value="Test"/>
    <property name="lastCalculation" value="10"/>
  </bean>
</beans>

我们必须在MBeanExporter bean 上指定 server 属性,以将其与已创建的MBeanServer关联起来。

随着 JDK 5.0 中注解的引入,Spring 使得可以使用注解将 Spring beans 注册为 JMX beans。

以下是使用@ManagedResource注解定义的Calculator bean 的示例:

package com.springhighperformance.jmx;

import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedOperationParameter;
import org.springframework.jmx.export.annotation.ManagedOperationParameters;
import org.springframework.jmx.export.annotation.ManagedResource;

  @ManagedResource(objectName = "Examples:type=JMX,name=Calculator",
    description = "A calculator to demonstrate JMX in the 
    SpringFramework")
  public class Calculator {
  private String name;
  private int lastCalculation;

  @ManagedAttribute(description = "Calculator name")
  public String getName() {
    return name;
  }

  @ManagedAttribute(description = "Calculator name")
  public void setName(String name) {
    this.name = name;
  }

  @ManagedAttribute(description = "The last calculation")
  public int getLastCalculation() {
    return lastCalculation;
  }

  @ManagedOperation(description = "Calculate two numbers")
  @ManagedOperationParameters({
      @ManagedOperationParameter(name = "x",
          description = "The first number"),
      @ManagedOperationParameter(name = "y",
          description = "The second number") })
  public void calculate(int x, int y) {
    lastCalculation = x + y;
  }
}

@ManagedAttribute@ManagedOperation注解用于将 bean 的属性和方法暴露给管理 bean 服务器。

以下是实例化受管 bean 的客户端代码,可以通过诸如 JConsole 或 VisualVM 之类的工具进行监视:

package com.springhighperformance.jmx;
import java.util.Random;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableMBeanExport;

@Configuration
@EnableMBeanExport
public class JmxSpringMain {
  private static final Random rand = new Random();

    @Bean
    public Resource jmxResource() {
        return new Resource();
    }

    @Bean
    public Calculator calculator() {
        return new Calculator();
    }

    public static void main(String[] args) throws InterruptedException {
        ApplicationContext context = new 
        AnnotationConfigApplicationContext(JmxSpringMain.class);
        do {
          Calculator cal = context.getBean(Calculator.class);
          cal.calculate(rand.nextInt(), rand.nextInt());
          Thread.sleep(Long.MAX_VALUE);
        } while(true);
    }
}

一旦暴露为受管 bean,这些资源可以通过诸如 JConsole 或 VisualVM 之类的工具监视各种参数,例如对象数量、对象占用的内存以及对象占用的堆内存空间。

以下是 Java VisualVM 的屏幕截图,突出显示了Calculator作为受管 bean 的暴露:

摘要

这是本书中最重要的章节之一。它专注于性能测量和增强策略。本章类似于现实生活中的健康检查场景。如果一个人不舒服,第一步是识别症状以便诊断和治疗疾病。同样,本章从识别性能下降的症状开始,然后进入性能调优生命周期。描述了性能调优模式和反模式,类似于应遵循的最佳实践。接着是迭代性能调优过程和 Spring 框架中的 JMX 支持。我们看到了 Spring bean 被转换为 JMX 受管 bean 的示例。

下一章重点介绍 JVM 的微调。这不是针对 Spring 应用程序特定的调整,而是适用于在 JVM 上运行的任何应用程序。本章将深入探讨 JVM 的内部细节,这些细节对开发人员来说并不是很熟悉。让我们准备好深入 JVM。

第十一章:JVM 内部

上一章让我们了解了如何通过理解性能问题的症状来调整应用程序的性能。我们走过了性能调整生命周期,学习了在应用程序性能的哪些阶段可以进行调整以及如何进行调整。我们还学会了如何将 JMX 连接到 Spring 应用程序,观察应用程序的瓶颈并进行调整。

在本章中,我们将深入了解Java 虚拟机JVM)的内部和调整 JVM 以实现高性能。JVM 执行两项主要工作——执行代码和管理内存。JVM 从操作系统分配内存,管理堆压缩,并对未引用的对象执行垃圾回收GC)。GC 很重要,因为适当的 GC 可以改善应用程序的内存管理和性能。

本章我们将学习以下主题:

  • 理解 JVM 内部

  • 理解内存泄漏

  • 常见陷阱

  • GC

  • GC 方法和策略

  • 分析 GC 日志的工具

理解 JVM 内部

作为 Java 开发人员,我们知道 Java 字节码在Java 运行环境JRE)中运行,而 JRE 最重要的部分是 JVM,它分析并执行 Java 字节码。当我们创建一个 Java 程序并编译它时,结果是一个扩展名为.class的文件。它包含 Java 字节码。JVM 将 Java 字节码转换为在我们运行应用程序的硬件平台上执行的机器指令。当 JVM 运行程序时,它需要内存来存储来自加载的类文件、实例化对象、方法参数、返回值、局部变量和计算的中间结果的字节码和其他信息。JVM 将它需要的内存组织成几个运行时数据区域。

JVM 由三部分组成:

  • 类加载器子系统

  • 内存区域

  • 执行引擎

以下图表说明了高级 JVM 架构:

JVM 架构

让我们简要了解一下图表中我们看到的 JVM 的三个不同部分。

类加载器子系统

类加载器子系统的责任不仅仅是定位和导入类的二进制数据。它还验证导入的类是否正确,为类变量分配和初始化内存,并协助解析符号引用。这些活动按严格顺序执行:

  1. 加载:类加载器读取.class文件并查找和导入类型的二进制数据。

  2. 链接:它执行验证、准备和(可选)解析:

  • 验证:确保导入类型的正确性

  • 准备:为类变量分配内存并将内存初始化为默认值

  • 解析:将类型的符号引用转换为直接引用

  1. 初始化:为代码中定义的所有静态变量分配值并执行静态块(如果有)。执行顺序是从类的顶部到底部,从类层次结构的父类到子类。

一般来说,有三个类加载器:

  • 引导类加载器:这加载位于JAVA_HOME/jre/lib目录中的核心可信 Java API 类。这些 Java API 是用本地语言(如 C 或 C++)实现的。

  • 扩展类加载器:这继承自引导类加载器。它从JAVA_HOME/jre/lib/ext目录或java.ext.dirs系统属性指定的任何其他目录加载类。它是由sun.misc.Launcher$ExtClassLoader类以 Java 实现的。

  • 系统类加载器:这继承自扩展类加载器。它从我们应用程序的类路径加载类。它使用java.class.path环境变量。

为了加载类,JVM 遵循委托层次原则。系统类加载器将请求委托给扩展类加载器,扩展类加载器将请求委托给引导类加载器。如果在引导路径中找到类,则加载该类,否则将请求转移到扩展类加载器,然后再转移到系统类加载器。最后,如果系统类加载器无法加载类,则会生成java.lang.ClassNotFoundException异常。

以下图表说明了委托层次原则:

委托层次原则

内存区域

Java 运行时内存分为五个不同的区域,如下图所示:

内存区域

让我们简要描述每个组件:

  • 方法区:这包含所有类级别的信息,如类名、父类、方法、实例和静态变量。每个 JVM 只有一个方法区,它是一个共享资源。

  • 堆区:这包含所有对象的信息。每个 JVM 有一个堆区。它也是一个共享资源。由于方法区堆区是多个线程之间的共享内存,所以存储的数据不是线程安全的。

  • 栈内存:JVM 为每个正在执行的线程创建一个运行时栈,并将其存储在栈区。这个栈的每个块被称为一个激活记录,用于存储方法调用。该方法的所有局部变量都存储在相应的帧中。栈区是线程安全的,因为它不是共享资源。运行时栈将在线程终止时由 JVM 销毁。因此,在方法调用的无限循环中,我们可能会看到StackOverFlowError,这是由于栈中没有足够的内存来存储方法调用。

  • PC 寄存器:这些保存正在执行的当前指令的地址。一旦指令执行完毕,PC 寄存器将被更新为下一条指令。每个线程有一个单独的PC 寄存器

  • 本地方法栈:为每个线程创建一个单独的本地栈。它存储本地方法信息。本地信息就是本地方法调用。

执行引擎

执行引擎在运行时数据区域执行字节码。它逐行执行字节码,并使用运行时数据区域中可用的信息。执行引擎可以分为三部分:

  • 解释器:这逐行读取、解释和执行字节码。它快速解释和执行字节码;然而,在执行解释结果时可能非常缓慢。

  • 即时(JIT):为了克服解释器在执行解释结果时的缓慢,即时编译器在解释器第一次解释代码后将字节码转换为本机代码。使用本机代码执行速度快;它逐条执行指令。

  • 垃圾收集器:这会销毁任何没有被引用的东西。这非常重要,因此任何不需要的东西都将被销毁,以便为新的执行腾出空间。

理解内存泄漏

Java 的最大好处是 JVM,它提供了开箱即用的内存管理。我们可以创建对象,Java 的垃圾收集器会帮我们释放内存。然而,在 Java 应用程序中会发生内存泄漏。在接下来的部分中,我们将看到一些内存泄漏的常见原因,并介绍一些检测/避免它们的解决方案。

Java 中的内存泄漏

当垃圾收集器无法收集应用程序不再使用/引用的对象时,就会发生内存泄漏。如果对象没有被垃圾收集,应用程序将使用更多内存,一旦整个堆区满了,对象就无法分配,导致OutOfMemoryError

堆内存有两种对象——被引用的对象和未被引用的对象。垃圾回收器会移除所有未被引用的对象。然而,垃圾回收器无法移除被引用的对象,即使它们没有被应用程序使用。

内存泄漏的常见原因

以下是内存泄漏的最常见原因:

  • 打开流:在处理流和读取器时,我们经常忘记关闭流,最终导致内存泄漏。未关闭流导致两种类型的泄漏——低级资源泄漏和内存泄漏。低级资源泄漏包括操作系统级资源,如文件描述符和打开连接。由于 JVM 消耗内存来跟踪这些资源,这导致内存泄漏。为了避免泄漏,使用finally块关闭流,或者使用 Java 8 的自动关闭功能。

  • 打开的连接:我们经常忘记关闭已打开的 HTTP、数据库或 FTP 连接,这会导致内存泄漏。与关闭流类似,要关闭连接。

  • 静态变量引用实例对象:任何引用重对象的静态变量都可能导致内存泄漏,因为即使变量没有被使用,它也不会被垃圾回收。为了防止这种情况发生,尽量不要使用重的静态变量,而是使用局部变量。

  • 集合中对象缺少方法:向HashSet中添加没有实现equalshashcode方法的对象会增加HashSet中重复对象的数量,一旦添加就无法移除这些对象。为了避免这种情况,在添加到HashSet中的对象中实现equalshashcode方法。

诊断内存泄漏是一个需要大量实际经验、调试技能和对应用程序的详细了解的漫长过程。以下是诊断内存泄漏的方法:

  • 启用 GC 日志并调整 GC 参数

  • 性能分析

  • 代码审查

在接下来的部分中,我们将看到 GC 的常见陷阱、GC 方法和分析 GC 日志的工具。

常见陷阱

性能调优至关重要,只需一个小的 JVM 标志,事情就可能变得复杂。JVM 会出现 GC 暂停,频率和持续时间各不相同。在暂停期间,一切都会停止,各种意外行为开始出现。在暂停和不稳定行为的情况下,JVM 被卡住,性能受到影响。我们可以看到响应时间变慢、CPU 和内存利用率高,或者系统大部分时间表现正常,但偶尔出现异常行为,比如执行极慢的事务和断开连接。

大部分时间我们测量平均事务时间,忽略导致不稳定行为的异常值。大部分时间系统表现正常,但在某些时刻,系统响应性下降。这种低性能的原因大部分是由于对 GC 开销的低意识和只关注平均响应时间。

在定义性能要求时,我们需要回答一个重要问题:与 GC 暂停频率和持续时间相关的应用程序的可接受标准是什么?要求因应用程序而异,因此根据我们的应用程序和用户体验,我们需要首先定义这些标准。

我们通常存在一些常见的误解。

垃圾回收器的数量

大多数时候,人们并不知道不只有一个,而是四个垃圾收集器。这四个垃圾收集器是——串行并行并发垃圾优先G1)。我们将在下一节中看到它们。还有一些第三方垃圾收集器,比如Shenandoah。JVM HotSpot 的默认垃圾收集器在 Java 8 之前是并行的,而从 Java 9 开始,默认收集器是垃圾优先垃圾收集器G1 GC)。并行垃圾收集器并不总是最好的;然而,这取决于我们的应用程序需求。例如,并发标记清除CMS)和 G1 收集器导致 GC 暂停的频率较低。但是当它们导致暂停时,暂停持续时间很可能比并行收集器导致的暂停时间长。另一方面,对于相同的堆大小,并行收集器通常能实现更高的吞吐量。

错误的垃圾收集器

GC 问题的一个常见原因是选择了错误的垃圾收集器。每个收集器都有其自己的重要性和好处。我们需要找出我们应用程序的行为和优先级,然后根据这些来选择正确的垃圾收集器。HotSpot 的默认垃圾收集器是并行/吞吐量,大多数情况下并不是一个好选择。CMS 和 G1 收集器是并发的,导致暂停的频率较低,但当暂停发生时,其持续时间比并行收集器长。因此,选择收集器是我们经常犯的一个常见错误。

并行/并发关键字

GC 可能会导致全局停顿STW)的情况,或者对象可以在不停止应用程序的情况下并发收集。GC 算法可以在单线程或多线程中执行。因此,并发 GC 并不意味着它是并行执行的,而串行 GC 并不意味着它由于串行执行而导致更多的暂停。并发和并行是不同的,其中并发表示 GC 周期,而并行表示 GC 算法。

G1 是一个问题解决者

随着 Java 7 引入新的垃圾收集器,许多人认为它是解决以前所有垃圾收集器问题的问题解决者。G1 GC 解决的一个重要问题是碎片问题,这是 CMS 收集器常见的问题。然而,在许多情况下,其他收集器可能会胜过 G1 GC。因此,一切取决于我们应用程序的行为和需求。

平均事务时间

大多数情况下,在测试性能时,我们倾向于测量平均事务时间,但仅这样做会忽略异常值。当 GC 导致长时间暂停时,应用程序的响应时间会急剧增加,影响用户访问应用程序。这可能会被忽视,因为我们只关注平均事务时间。当 GC 暂停频率增加时,响应时间成为一个严重的问题,我们可能会忽略只测量平均响应时间而忽略的问题。

减少新对象分配率可以改善 GC 行为

与其专注于减少新对象分配率,我们应该专注于对象的生命周期。有三种不同类型的对象生命周期:长期对象,我们对它们无能为力;中期对象,这些会导致最大的问题;和短期对象,通常会被快速释放和分配,因此它们会在下一个 GC 周期中被收集。因此,与其专注于长期和短期对象,专注于中期对象的分配率可能会带来积极的结果。问题不仅仅在于对象分配率,而是在于所涉及的对象类型。

GC 日志会导致开销

GC 日志并不会导致开销,尤其是在默认日志设置中。这些数据非常有价值,Java 7 引入了控制日志文件大小的钩子。如果我们不收集带有时间戳的 GC 日志,那么我们就错过了分析和解决暂停问题的关键数据来源。GC 日志是系统中 GC 状态的最丰富的数据来源。我们可以获得关于应用程序中所有 GC 事件的数据;比如,它是并发完成的还是导致了 STW 暂停:花了多长时间,消耗了多少 CPU,释放了多少内存。通过这些数据,我们将能够了解暂停的频率和持续时间,它们的开销,并采取行动来减少它们。

通过添加以下参数启用 GC:

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:`date +%F_%H-%M-%S`-gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=10M

GC

Java 最大的成就之一就是 GC。GC 进程自动管理内存和堆分配,跟踪死对象,删除它们,并将内存重新分配给新对象。理论上,由于垃圾收集器自动管理内存,开发人员可以创建新对象而不必考虑内存的分配和释放,以消除内存泄漏和其他与内存相关的问题。

GC 的工作原理

通常我们认为 GC 收集并删除未引用的对象。相反,Java 中的 GC 跟踪活动对象,并将所有未引用的对象标记为垃圾。

内存的堆区是动态分配对象的地方。在运行应用程序之前,我们应该为 JVM 分配堆内存。提前为 JVM 分配堆会产生一些后果:

  • 提高对象创建速率,因为 JVM 不需要与操作系统通信为每个新对象获取内存。一旦 JVM 为对象分配了内存,JVM 就会将指针移向下一个可用内存。

  • 当没有对象引用时,垃圾收集器收集对象并重用其内存以分配新对象。由于垃圾收集器不删除对象,因此不会将内存返回给操作系统。

直到对象被引用,JVM 认为它们是活动对象。当一个对象不再被引用并且不可被应用程序代码访问时,垃圾收集器将其删除并回收其内存。我们会想到一个问题,对象树中的第一个引用是谁?让我们看看对象树及其根。

GC 根

对象的每个树都有一个或多个对象作为根。如果垃圾收集器可以到达根,那么该树是可达的。任何未被 GC 根引用或引用的对象都被视为死对象,垃圾收集器将其删除。

以下是 Java 中不同类型的 GC 根:

  • 局部变量:Java 方法的变量或参数。

  • 活动线程:正在运行的线程是一个活动对象。

  • 静态变量:引用静态变量的类。当垃圾收集器收集类时,它会删除对静态变量的引用。

  • JNI 引用:在 JNI 调用期间创建的对象引用。它们保持活动状态,因为 JVM 不知道本地代码对它的引用。

请看下面的图表:

GC 根

GC 方法和策略

正如我们在前面的部分中学到的,不只有一个,而是四种不同的垃圾收集器。每种都有其自己的优点和缺点。这些收集器共同的一点是它们将托管堆分成不同的段,假设对象的寿命很短,应该很快被移除。让我们看看 GC 的四种不同算法。

串行收集器

串行收集器是最简单的 GC 实现,主要设计用于单线程环境和小堆。这种 GC 实现在工作时会冻结所有应用程序线程。因此,在多线程应用程序中使用它并不是一个好主意,比如服务器环境。

要启用串行垃圾收集器,请将-XX:+UseSerialGC设置为 VM 参数

并行/吞吐量收集器

并行收集器是 JVM 的默认收集器,也被称为吞吐量收集器。顾名思义,这个收集器与串行收集器不同,它使用多线程来管理堆内存。并行垃圾收集器在执行部分或完整的 GC 时仍会冻结所有应用程序线程。如果我们想使用并行垃圾收集器,我们应该指定调优参数,如线程、暂停时间、吞吐量和占用空间。

以下是指定调优参数的参数:

  • 线程:-XX:ParallelGCThreads=<N>

  • 暂停时间:-XX:MaxGCPauseMillis=<N>

  • 吞吐量:-XX:GCTimeRatio=<N>

  • 占用空间(最大堆大小):-Xmx<N>

要在我们的应用程序中启用并行垃圾收集器,请设置-XX:+UseParallelGC选项。

CMS 垃圾收集器

CMS 实现使用多个垃圾收集器线程来扫描(标记)可以被移除的未使用对象(清除)。这种垃圾收集器适用于需要短暂 GC 暂停的应用程序,并且在应用程序运行时可以与垃圾收集器共享处理器资源。

CMS 算法只在两种情况下进入 STW 模式:当 Old Generations 中的对象仍然被线程入口点或静态变量引用时,以及当应用程序在 CMS 运行时改变了堆的状态,使算法返回并重新迭代对象树以验证它已标记正确的对象。

使用这个收集器,晋升失败是最大的担忧。晋升失败发生在 Young 和 Old Generations 的对象收集之间发生竞争条件时。如果收集器需要将对象从 Young Generation 晋升到 Old Generation,而没有足够的空间,它必须首先 STW 来创建空间。为了确保在 CMS 收集器的情况下不会发生这种情况,增加 Old Generation 的大小或为收集器分配更多的后台线程来与分配速率竞争。

为了提供高吞吐量,CMS 使用更多的 CPU 来扫描和收集对象。这对于长时间运行的服务器应用程序是有利的,这些应用程序不希望应用程序冻结。因此,如果我们可以分配更多的 CPU 来避免应用程序暂停,我们可以选择 CMS 收集器作为应用程序中的 GC。要启用 CMS 收集器,请设置-XX:+UseConcMarkSweepGC选项。

G1 收集器

这是在 JDK 7 更新 4 中引入的新收集器。G1 收集器设计用于愿意分配超过 4GB 堆内存的应用程序。G1 将堆分成多个区域,跨越从 1MB 到 32MB 的范围,取决于我们配置的堆,并使用多个后台线程来扫描堆区域。将堆分成多个区域的好处是,G1 将首先扫描有大量垃圾的区域,以满足给定的暂停时间。

G1 减少了后台线程完成未使用对象扫描之前低堆可用性的机会。这减少了 STW 的机会。G1 在进行堆压缩时是动态的,而 CMS 是在 STW 期间进行的。

为了在我们的应用程序中启用 G1 垃圾收集器,我们需要在 JVM 参数中设置-XX:+UseG1GC选项。

Java 8 更新 20 引入了一个新的 JVM 参数,-XX:+UseStringDeduplication,用于 G1 收集器。通过这个参数,G1 识别重复的字符串,并创建指向相同的char[]数组的指针,以避免多个相同字符串的副本。

从 Java 8 开始,PermGen 的一部分堆被移除。这部分堆是为类元数据、静态变量和 interned 字符串分配的。这种参数调优导致了许多OutOfMemory异常,在 Java 8 之后,JVM 会处理这些异常。

堆内存

堆内存主要分为两代:年轻代和老年代。在 Java 7 之前,堆内存中有一个PERM GENERATION,而从 Java 8 开始,PERM GENERATIONMETASPACE取代。METASPACE不是堆内存的一部分,而是本地内存的一部分。使用-XX:MaxMetaspaceSize选项设置METASPACE的大小。在投入生产时,考虑此设置至关重要,因为如果METASPACE占用过多内存,会影响应用程序的性能:

Java 8 内存管理

年轻代是对象创建和分配的地方;它是为年轻对象而设的。年轻代进一步分为幸存者空间。以下是Hotspot 堆结构

伊甸园区域默认比幸存者空间大。所有对象首先都是在伊甸园区域中创建的。当伊甸园满时,将触发小型 GC,它将快速扫描对象的引用,并标记未引用的对象为死亡并进行收集。幸存者空间中的任何一个区域始终为空。在小型 GC 期间在伊甸园中幸存的对象将被移至空的幸存者空间。我们可能会想为什么有两个幸存者空间而不是一个。原因是为了避免内存碎片化。当年轻代运行并从幸存者空间中删除死对象时,会在内存中留下空洞并需要压缩。为了避免压缩,JVM 将幸存对象从一个幸存者空间移至另一个。这种从伊甸园和一个幸存者空间到另一个的活对象的乒乓运动会持续,直到出现以下条件:

  • 对象达到最大 tenuring 阈值。这意味着对象不再年轻。

  • 幸存者空间已满,无法容纳任何新对象。

当出现上述条件时,对象将被移至老年代

JVM 标志

以下是应用程序中常用的用于调整 JVM 以获得更好性能的 JVM 参数/标志。调整值取决于我们应用程序的行为以及生成速率。因此,没有明确定义的指南来使用特定值的 JVM 标志以实现更好的性能。

-Xms 和-Xmx

-Xms-Xmx被称为最小和最大堆大小。将-Xms设置为等于-Xmx可以防止堆扩展时的 GC 暂停,并提高性能。

-XX:NewSize 和-XX:MaxNewSize

我们可以使用-XX:MaxNewSize设置年轻代的大小。如果我们将年轻代的大小设置得很大,那么老年代的大小将会较小。出于稳定性的原因,年轻代的大小不应该大于老年代。因此,-Xmx/2是我们可以为-XX:MaxNewSize设置的最大大小。

为了获得更好的性能,通过设置-XX:NewSize标志来设置年轻代的初始大小。这样可以节省一些成本,因为年轻代随着时间的推移会增长到该大小。

-XX:NewRatio

我们可以使用-XX:NewRatio选项将年轻代的大小设置为老年代的比例。使用此选项的好处可能是,当 JVM 在执行过程中调整总堆大小时,年轻代可以增长和收缩。-XX:NewRatio表示老年代的比例大于年轻代。-XX:NewRatio=2表示老年代的大小是年轻代的两倍,这进一步意味着年轻代是总堆的 1/3。

如果我们指定了 Young Generation 的比例和固定大小,那么固定大小将优先。关于指定 Young Generation 大小的方法没有一定的规则。这里的经验法则是,如果我们知道应用程序生成的对象的大小,那么指定固定大小,否则指定比例。

-XX:SurvivorRatio

-XX:SurvivorRatio值是 Eden 相对于 Survivor Spaces 的比例。将有两个 Survivor Spaces,每个都相等。如果-XX:SurvivorRatio=8,那么 Eden 占 3/4,每个 Survivor Spaces 占老年代总大小的 1/4。

如果我们设置了 Survivor Spaces 很小的比例,那么 Eden 将为新对象腾出更多空间。在 Minor GC 期间,未引用的对象将被收集,Eden 将为空出来给新对象,但是如果对象仍然有引用,垃圾收集器会将它们移动到 Survivor Space。如果 Survivor Space 很小,无法容纳新对象,那么对象将被移动到老年代。老年代中的对象只能在 Full GC 期间被收集,这会导致应用程序长时间暂停。如果 Survivor Space 足够大,那么更多的对象可以存活在 Survivor Space 中,但会死得很快。如果 Survivor Spaces 很大,Eden 将会很小,而小的 Eden 会导致频繁的 Young GC。

-XX:InitialTenuringThreshold、-XX:MaxTenuringThreshold 和-XX:TargetSurvivorRatio

Tenuring 阈值决定了对象何时可以从 Young Generation 晋升/移动到 Old Generation。我们可以使用-XX:InitialTenuringThreshold-XX:MaxTenuringThreshold JVM 标志来设置 tenuring 阈值的初始值和最大值。我们还可以使用-XX:TargetSurvivorRatio来指定 Young Generation GC 结束时 Survivor Space 的目标利用率(以百分比表示)。

-XX:CMSInitiatingOccupancyFraction

当使用 CMS 收集器(-XX:+UseConcMarkSweepGC)时,使用-XX:CMSInitiatingOccupancyFraction=85选项。如果设置了该标志,并且老年代占用了 85%,CMS 收集器将开始收集未引用的对象。并不是必须老年代占用了 85% CMS 才开始收集。如果我们希望 CMS 只在 85%时开始收集,那么需要设置-XX:+UseCMSInitiatingOccupancyOnly-XX:CMSInitiatingOccupancyFraction标志的默认值为 65%。

-XX:+PrintGCDetails、-XX:+PrintGCDateStamps 和-XX:+PrintTenuringDistribution

设置标志以生成 GC 日志。为了微调 JVM 参数以实现更好的性能,了解 GC 日志和应用程序的行为非常重要。-XX:+PrintTenuringDistribution报告对象的统计信息(它们的年龄)以及它们晋升时的期望阈值。这对于了解我们的应用程序如何持有对象非常重要。

分析 GC 日志的工具

Java GC 日志是我们在性能问题发生时可以开始调试应用程序的地方之一。GC 日志提供重要信息,例如:

  • GC 上次运行的时间

  • GC 循环运行的次数

  • GC 运行的间隔

  • GC 运行后释放的内存量

  • GC 运行的时间

  • 垃圾收集器运行时 JVM 暂停的时间

  • 分配给每个代的内存量

以下是样本 GC 日志:

2018-05-09T14:02:17.676+0530: 0.315: Total time for which application threads were stopped: 0.0001783 seconds, Stopping threads took: 0.0000239 seconds
2018-05-09T14:02:17.964+0530: 0.603: Application time: 0.2881052 seconds
.....
2018-05-09T14:02:18.940+0530: 1.579: Total time for which application threads were stopped: 0.0003113 seconds, Stopping threads took: 0.0000517 seconds
2018-05-09T14:02:19.028+0530: 1.667: Application time: 0.0877361 seconds
2018-05-09T14:02:19.028+0530: 1.667: [GC (Allocation Failure) [PSYoungGen: 65536K->10723K(76288K)] 65536K->13509K(251392K), 0.0176650 secs] [Times: user=0.05 sys=0.00, real=0.02 secs] 
2018-05-09T14:02:19.045+0530: 1.685: Total time for which application threads were stopped: 0.0179326 seconds, Stopping threads took: 0.0000525 seconds
2018-05-09T14:02:20.045+0530: 2.684: Application time: 0.9992739 seconds
.....
2018-05-09T14:03:54.109+0530: 96.748: Total time for which application threads were stopped: 0.0000498 seconds, Stopping threads took: 0.0000171 seconds
Heap
 PSYoungGen total 76288K, used 39291K [0x000000076b200000, 0x0000000774700000, 0x00000007c0000000)
  eden space 65536K, 43% used [0x000000076b200000,0x000000076cde5e30,0x000000076f200000)
  from space 10752K, 99% used [0x000000076f200000,0x000000076fc78e28,0x000000076fc80000)
  to space 10752K, 0% used [0x0000000773c80000,0x0000000773c80000,0x0000000774700000)
 ParOldGen total 175104K, used 2785K [0x00000006c1600000, 0x00000006cc100000, 0x000000076b200000)
  object space 175104K, 1% used [0x00000006c1600000,0x00000006c18b86c8,0x00000006cc100000)
 Metaspace used 18365K, capacity 19154K, committed 19456K, reserved 1067008K
  class space used 2516K, capacity 2690K, committed 2816K, reserved 1048576K
2018-05-09T14:03:54.123+0530: 96.761: Application time: 0.0131957 seconds

这些日志很难快速解释。如果有一个工具可以将这些日志呈现在可视化界面中,那么就可以轻松快速地理解 GC 的情况。我们将在下一节中看一下这样的工具来解释 GC 日志。

GCeasy

GCeasy 是最受欢迎的垃圾收集日志分析工具之一。GCeasy 被开发出来自动从 GC 日志中识别问题。它足够智能,可以提供解决问题的替代方法。

以下是 GCeasy 提供的重要基本功能:

  • 使用机器学习算法分析日志

  • 快速检测内存泄漏、过早对象晋升、长时间的 JVM 暂停以及许多其他性能问题

  • 功能强大且信息丰富的可视化分析工具

  • 提供用于主动日志分析的 REST API

  • 免费的基于云的日志分析工具

  • 提供有关 JVM 堆大小的建议

  • 能够分析所有格式的 GC 日志

GCeasy.io (www.gceasy.io/)是在线垃圾收集日志分析工具。它需要将日志文件上传到 GCeasy 公共云。

使用在线工具收集详细的日志分析的步骤如下:

  1. 通过在服务器的 JVM 参数中添加XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:<GC-log-file-path>来在应用程序中启用 GC 日志。

  2. 一旦在指定位置生成了 GC 日志文件,通过导航到gceasy.io/将文件上传到 GCeasy 云。如果有多个日志文件需要分析,也可以上传压缩的 ZIP 文件。

  3. 处理日志文件后,将生成详细的分析报告。

报告组织得当且详细到足以突出每一个可能导致性能下降的问题。以下部分解释了 GCeasy 生成的报告中的重要部分。

JVM 调优提示

报告中的顶部部分根据垃圾收集日志分析提供建议。这些建议是通过机器学习算法动态生成的,经过对日志文件的彻底分析。建议中的细节还包括问题的可能原因。以下是 GCeasy 在 GC 日志分析后提供的一个示例建议:

JVM 堆大小

报告中的这一部分提供了每个内存代的堆分配和峰值内存使用情况的信息。可能分配的堆大小可能与 JVM 参数中定义的大小不匹配。这是因为 GCeasy 工具从日志中获取了分配的内存信息。可能我们分配了 2GB 的堆内存,但在运行时,JVM 只分配了 1GB 的堆内存。在这种情况下,报告将显示分配的内存为 1GB。报告以表格和图形格式显示堆分配。以下是报告中堆大小部分的示例:

关键绩效指标

关键绩效指标KPIs)有助于做出改善应用程序性能的深刻决策。吞吐量、延迟和占用空间是一些重要的 KPIs。报告中的 KPIs 包括吞吐量和延迟。占用空间基本上描述了 CPU 占用的时间。它可以从性能监控工具(如 JVisualVM)中获取。

吞吐量选项表示在指定时间段内应用程序完成的有效工作量。延迟选项表示 GC 运行所花费的平均时间。

以下是报告中 KPIs 的示例:

GC 统计

GC 统计部分提供了一段时间内垃圾收集器的行为信息。这段时间是分析日志的时间段。GC 统计是基于实时分析提供的。统计数据包括垃圾收集器运行后回收的字节数、累积 GC 时间(以秒为单位)和平均 GC 时间(以秒为单位)。该部分还以表格格式提供了有关总 GC 统计、小型 GC 统计和完整 GC 统计以及 GC 暂停统计的信息。

GC 原因

GC Causes 部分提供了有关垃圾收集器运行原因的信息。该信息以表格和图形格式提供。除了原因,它还提供了垃圾收集器执行所需的时间信息。以下是报告中的一个示例:

基于上述细节,GCeasy 是一个帮助开发人员以可视化方式解释 GC 日志的重要工具。

摘要

在本章中,我们学习了 JVM 及其参数。我们了解了内存泄漏以及与 GC 相关的常见误解。我们了解了不同的 GC 方法及其重要性。我们了解了重要的 JVM 标志,这些标志被调整以实现更好的性能。

在下一章中,我们将学习关于 Spring Boot 微服务及其性能调优。微服务是一种应用架构,它由松散耦合的服务实现业务功能。Spring Boot 使我们能够构建生产就绪的应用程序。

第十二章:Spring Boot 微服务性能调优

在上一章中,我们了解了Java 虚拟机JVM)。从 JVM 的内部和 Java 的类加载机制开始,我们了解了 Java 中的内存管理是如何进行的。本章的最后一节关注了垃圾回收和 JVM 调优。本章充满了对应用程序性能优化非常重要的细节。

在本章中,我们将着手解决性能问题。方法是开发微服务。微服务目前在软件开发行业中非常流行。微服务和相关关键词引起了很多关注。这种方法基本上是在应用架构层面调整应用程序的性能。它描述了我们如何通过以不同的方式设置架构来改善应用程序的性能。本章将涵盖以下主题:

  • Spring Boot 配置

  • Spring Boot 执行器的指标

  • 健康检查

  • 使用 Spring Boot 的微服务

  • 使用 Spring Cloud 的微服务

  • Spring 微服务配置示例

  • 使用 Spring Boot admin 监控微服务

  • Spring Boot 性能调优

Spring Boot 配置

在本节中,我们将专注于让 Spring Boot 为我们工作。在跳转到 Spring Boot 配置之前,我们将了解 Spring Boot 是什么,为什么我们应该使用它,以及 Spring Boot 带来了什么。我们将迅速转向如何做这一部分。

什么是 Spring Boot?

软件开发过程需要更快、更准确、更健壮。要求软件团队快速开发原型,展示应用程序的功能给潜在客户。对生产级应用程序也是如此。以下是软件架构师关注的一些领域,以提高开发团队的效率:

  • 使用正确的一套工具,包括框架、IDE 和构建工具

  • 减少代码混乱

  • 减少编写重复代码的时间

  • 大部分时间用于实现业务功能

让我们思考一下。为什么我们要讨论这个?原因是这是 Spring Boot 的基础。这些想法是任何帮助团队提高生产力的框架或工具的基石。Spring Boot 也是出于同样的原因而存在——提高生产力!

使用 Spring Boot,轻松创建由 Spring 框架驱动的生产级应用程序。它还可以轻松创建具有最小挑战的生产就绪服务。Spring Boot 通过对 Spring 框架持有一种看法,帮助新用户和现有用户快速进行生产任务。Spring Boot 是一个工具,可以帮助创建一个独立的 Java 应用程序,可以使用java -jar命令运行,或者一个可以部署到 Web 服务器的 Web 应用程序。Spring Boot 设置捆绑了命令行工具来运行 Spring 程序。

Spring Boot 的主要目标是:

  • 以极快的速度开始使用 Spring 项目

  • 广泛的可访问性

  • 主要支持开箱即用的配置

  • 根据需要灵活地偏离 Spring 默认设置

  • 不生成任何代码

  • 不需要 XML 配置

除了前面列出的主要特性,Spring Boot 还提供了以下非功能特性的支持:

  • 支持广为人知和使用的框架的版本和配置

  • 应用安全支持

  • 监控应用程序健康检查参数的支持

  • 性能指标监控支持

  • 外部化配置支持

尽管 Spring Boot 为主要和非功能特性提供了默认值,但它足够灵活,允许开发人员使用他们选择的框架、服务器和工具。

Spring Initializr

Spring Boot 应用程序可以以多种方式启动。其中一种方式是使用基于 Eclipse 的 Spring 工具套件 IDE (spring.io/tools/sts)。另一种方式是使用start.spring.io,也称为 Spring Initializr。首先,Spring Initializr 不是 Spring Boot 或等效物。Spring Initializr 是一个具有简单 Web UI 支持的工具,用于配置 Spring Boot 应用程序。它可以被认为是一个用于快速启动生成 Spring 项目的工具。它提供了可以扩展的 API,以便生成项目的定制化。

Spring Initializr 工具提供了一个配置结构,用于定义依赖项列表、支持的 Java 和 Spring Boot 版本以及支持的依赖项版本。

基本上,Spring Initializr 根据提供的配置创建一个初始的 Spring 项目,并允许开发人员下载 ZIP 文件中的项目。以下是要遵循的步骤:

  1. 导航到start.spring.io/

  2. 从 Maven 或 Gradle 中选择依赖项管理工具。

  3. 从 Java、Kotlin 和 Groovy 中选择基于 JVM 的编程语言。

  4. 选择要使用的 Spring Boot 版本。

  5. 通过输入组名com.packt.springhighperformance来提供组件名称。

  6. 输入 Artifact,这是 Maven 项目的 Artifact ID。这将成为要部署或执行的项目 WAR 或 JAR 文件的名称。

  7. 从 Jar 和 War 中选择一种打包类型。

  8. 单击“切换到完整版本”链接。这将打开一个可供选择的起始项目列表。起始项目将在下一节中详细解释。

  9. 一旦我们选择了起始器或依赖项,点击“生成项目”按钮。这将下载包含初始项目配置的 ZIP 文件。

以下是带有一些配置的 Spring Initializr 屏幕:

完成后,将生成类似于以下截图所示的文件夹结构:

Spring Initializr 还支持命令行界面来创建 Spring 项目配置。可以使用以下命令来生成项目配置:

> curl https://start.spring.io/starter.zip -d dependencies=web,jpa -d bootVersion=2.0.0 -o ch-09-spring-boot-example-1.zip

正如前面提到的,Spring Initializr 支持与 IDE 的集成。它与 Eclipse/STS、IntelliJ ultimate 版和带有 NB SpringBoot 插件的 NetBeans 集成良好。

使用 Maven 的起始器

在前面的部分中,我们看了 Spring Initializr 工具。现在是时候快速查看 Spring Boot 支持的起始器或依赖项了。

随着项目复杂性的增加,依赖项管理变得具有挑战性。建议不要为复杂项目手动管理依赖项。Spring Boot 起始器解决了类似的问题。Spring Boot 起始器是一组依赖描述符,可以在使用 starter POMs 的 Spring 应用程序中包含。它消除了寻找示例代码和复制/粘贴大量 Spring 和相关库的依赖描述符的需要。例如,如果我们想要使用 Spring 和 JPA 开发应用程序,我们可以在项目中包含spring-boot-data-jpa-starter依赖项。spring-boot-data-jpa-starter是其中的一个起始器。这些起始器遵循统一的命名模式,例如spring-boot-starter-*,其中*表示应用程序的类型。

以下是一些 Spring Boot 应用程序起始器的列表:

名称 描述
spring-boot-starter 核心起始器提供自动配置和日志记录支持。
spring-boot-starter-activemq 使用 Apache ActiveMQ 的 JMS 消息起始器。
spring-boot-starter-amqp Spring AMQP 和 Rabbit MQ 起始器。
spring-boot-starter-aop Spring AOP 和 AspectJ 起始器。
spring-boot-starter-artemis 使用 Apache Artemis 的 JMS 消息起始器。
spring-boot-starter-batch Spring Batch 起始器。
spring-boot-starter-cache Spring Framework 的缓存支持。
spring-boot-starter-cloud-connectors 提供支持,使用 Spring Cloud Connectors 在云平台(如 Cloud Foundry 和 Heroku)中简化与云服务的连接。
spring-boot-starter-data-elasticsearch 具有对 elasticsearch 和分析引擎以及 Spring Data Elasticsearch 的支持的启动器。
spring-boot-starter-data-jpa 使用 Hibernate 的 Spring Data JPA。
spring-boot-starter-data-ldap Spring Data LDAP。
spring-boot-starter-data-mongodb MongoDB 文档导向数据库和 Spring Data MongoDB。
spring-boot-starter-data-redis 使用 Spring Data Redis 和 Lettuce 客户端的 Redis 键值数据存储。
spring-boot-starter-data-rest 提供支持,使用 Spring Data REST 在 REST 上公开 Spring Data 存储库的启动器。
spring-boot-starter-data-solr 使用 Spring Data Solr 的 Apache Solr 搜索平台。
spring-boot-starter-freemarker 支持使用 FreeMarker 视图构建 MVC Web 应用程序的启动器。
spring-boot-starter-groovy-templates 支持使用 Groovy 模板视图构建 MVC Web 应用程序的启动器。
spring-boot-starter-integration Spring Integration。
spring-boot-starter-jdbc 使用 Tomcat JDBC 连接池的 JDBC。
spring-boot-starter-jersey 支持使用 JAX-RS 和 Jersey 构建 RESTful Web 应用程序。这是spring-boot-starter-web starter的替代品。
spring-boot-starter-json 支持 JSON 操作的启动器。
spring-boot-starter-mail 支持使用 Java Mail 和 Spring Framework 的邮件发送支持的启动器。
spring-boot-starter-quartz 用于使用 Spring Boot Quartz 的启动器。
spring-boot-starter-security Spring Security 启动器。
spring-boot-starter-test 支持使用包括 JUnit、Hamcrest 和 Mockito 在内的库的 Spring Boot 应用程序。
spring-boot-starter-thymeleaf 支持使用 Thymeleaf 视图构建 MVC Web 应用程序。
spring-boot-starter-validation 使用 Hibernate Validator 支持 Java Bean 验证的启动器。
spring-boot-starter-web 支持使用 Spring MVC 构建 Web 应用程序,包括 RESTful 应用程序。它使用 Tomcat 作为默认的嵌入式容器。
spring-boot-starter-web-services 支持使用 Spring Web Services。
spring-boot-starter-websocket 支持使用 Spring Framework 的 WebSocket 支持构建 WebSocket 应用程序。

spring-boot-starter-actuator 是 Spring Boot Actuator 工具的生产启动器,提供了生产就绪功能的支持,如应用程序监控、健康检查、日志记录和 bean。

以下列表包括 Spring Boot 的一些技术启动器:

名称 描述
spring-boot-starter-jetty  作为嵌入式 Servlet 容器的 Jetty 支持。这是spring-boot-starter-tomcat的替代品。
spring-boot-starter-log4j2 支持 Log4j 2 进行日志记录。这是spring-boot-starter-logging的替代品。
spring-boot-starter-logging 这是使用 logback 的默认日志启动器。
spring-boot-starter-tomcat 这是用于spring-boot-starter-web的默认 Servlet 容器启动器。它使用 Tomcat 作为嵌入式服务器。
spring-boot-starter-undertow 这是spring-boot-starter-tomcat starter的替代品。它使用 Undertow 作为嵌入式服务器。
spring-boot-starter-cache Spring Framework 的缓存支持。

创建您的第一个 Spring Boot 应用程序

在本节中,我们将查看开发 Spring Boot 应用程序的先决条件。我们将开发一个小型的 Spring Boot 应用程序,以了解 Spring Boot 应用程序所需的配置和每个配置的重要性。

以下是使用 Spring Boot 的先决条件列表:

  • Java 8 或 9

  • Spring 5.0.4 或更高版本

Spring Boot 支持:

  • Maven 3.2+和 Gradle 4 用于依赖管理和显式构建

  • Tomcat 8.5,Jetty 9.4 和 Undertow 1.4

Spring Boot 应用程序可以部署到任何 servlet 3.0+兼容的 servlet 容器。

开发 Spring Boot 应用程序的第一步是安装 Spring Boot。设置非常简单。它可以像其他标准 Java 库一样设置。要安装 Spring Boot,我们需要在类路径中包含适当的spring-boot-*.jar库文件。Spring Boot 不需要任何专门的工具,可以使用任何 IDE 或文本编辑器。

虽然我们可以将所需的 Spring Boot JAR 文件复制到应用程序类路径中,但建议使用构建工具,如 Maven 或 Gradle,进行依赖管理。

Spring Boot 依赖项使用的 Maven groupIdorg.springframework.boot。对于 Spring Boot 应用程序,Maven POM 文件继承了spring-boot-starter-parent项目。Spring Boot 定义了启动器项目,并在 Spring Boot 应用程序的依赖项中定义为依赖项。

让我们开始创建我们的第一个 Spring Boot 应用程序,按照以下步骤进行:

  1. 使用 Spring Initializr 创建一个 kickstarter 应用程序。

  2. 选择 Maven 作为构建和依赖管理工具。

  3. 选择适当的 Spring Boot 版本。

  4. 选择打包类型为 War。

  5. 为了简单起见,我们将不在应用程序中包含 JPA 启动器。我们只会包含一个 web 模块,以演示一个请求-响应流程。

  6. 下载并导入项目到 STS 或 Eclipse。

  7. 在 STS 中,您可以将应用程序作为 Spring Boot 应用程序运行,而在 Eclipse 中,您可以选择将应用程序作为 Java 应用程序运行。

现在让我们浏览一下代码片段。以下是示例 Maven POM 文件:

<?xml version="1.0" encoding="UTF-8"?>
<project  

    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0         
    http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.packt.springhighperformance.ch09</groupId>
  <artifactId>ch-09-boot-example</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>boot-example</name>
  <description>Demo project for Spring boot</description>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-
    8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <spring-cloud.version>Finchley.M9</spring-cloud.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

在前面的配置文件中,一个值得注意的配置是父依赖项。如前所述,所有 Spring Boot 应用程序在pom.xml文件中使用spring-boot-starter-parent作为父依赖项。

父 POM 帮助管理子项目和模块的以下内容:

  • Java 版本

  • 包含依赖项的版本管理

  • 插件的默认配置

Spring Boot 父启动器将 Spring Boot 依赖项定义为父 POM。因此,它从 Spring Boot 依赖项继承了依赖项管理功能。它将默认的 Java 版本定义为 1.6,但在项目级别上,我们可以将其更改为1.8,如前面的代码示例所示。

除了默认的 POM 文件外,Spring Boot 还创建了一个作为应用程序启动器的 Java 类。以下是示例 Java 代码:

package com.packt.springhighperformance.ch09;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BootExampleApplication {

  public static void main(String[] args) {
    SpringApplication.run(BootExampleApplication.class, args);
  }
}

SpringApplication是一个负责引导 Spring Boot 应用程序的类。

Spring Boot 应用程序开发人员习惯于使用@Configuration@EnableAutoConfiguration@ComponentScan注解来注释主应用程序类。以下是每个注解的简要描述:

  • @Configuration:这是一个 Spring 注解,不特定于 Spring Boot 应用程序。它表示该类是 bean 定义的来源。

  • @EnableAutoConfiguration:这是一个 Spring Boot 特定的注解。该注解使应用程序能够从类路径定义中添加 bean。

  • @ComponentScan:此注解告诉 Spring 应用程序在提供的搜索路径中搜索组件、配置和服务。

以下是@SpringBootApplication注解的定义:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Configuration
@EnableAutoConfiguration
@ComponentScan
public @interface SpringBootApplication {
......

从前面的代码可以看出,@SpringBootApplication作为一个方便的注解来定义 Spring Boot 应用程序,而不是声明三个注解。

以下代码块显示了当 Spring Boot 应用程序启动时的日志输出:


  . ____ _ __ _ _
 /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/ ___)| |_)| | | | | || (_| | ) ) ) )
  ' |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot :: (v2.0.0.RELEASE)

2018-05-23 16:29:21.382 INFO 32268 --- [ main] c.p.s.ch09.BootExampleApplication : Starting BootExampleApplication on DESKTOP-4DS55MC with PID 32268 (E:\projects\spring-high-performance\ch-09\boot-example\target\classes started by baps in E:\projects\spring-high-performance\ch-09\boot-example)
2018-05-23 16:29:21.386 INFO 32268 --- [ main] c.p.s.ch09.BootExampleApplication : No active profile set, falling back to default profiles: default
2018-05-23 16:29:21.441 INFO 32268 --- [ main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@58ce9668: startup date [Wed May 23 16:29:21 IST 2018]; root of context hierarchy
2018-05-23 16:29:23.854 INFO 32268 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2018-05-23 16:29:23.881 INFO 32268 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2018-05-23 16:29:23.881 INFO 32268 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.28
2018-05-23 16:29:23.888 INFO 32268 --- [ost-startStop-1] o.a.catalina.core.AprLifecycleListener : The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: ...
2018-05-23 16:29:24.015 INFO 32268 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2018-05-23 16:29:24.016 INFO 32268 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 2581 ms
2018-05-23 16:29:25.011 INFO 32268 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Servlet dispatcherServlet mapped to [/]
2018-05-23 16:29:25.015 INFO 32268 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-05-23 16:29:25.016 INFO 32268 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-05-23 16:29:25.016 INFO 32268 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-05-23 16:29:25.016 INFO 32268 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*]
2018-05-23 16:29:25.016 INFO 32268 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpTraceFilter' to: [/*]
2018-05-23 16:29:25.016 INFO 32268 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'webMvcMetricsFilter' to: [/*]
2018-05-23 16:29:26.283 INFO 32268 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/welcome]}" onto public java.lang.String com.packt.springhighperformance.ch09.controllers.MainController.helloMessage(java.lang.String)
2018-05-23 16:29:26.284 INFO 32268 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/]}" onto public java.lang.String com.packt.springhighperformance.ch09.controllers.MainController.helloWorld()
2018-05-23 16:29:26.291 INFO 32268 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-05-23 16:29:26.292 INFO 32268 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-05-23 16:29:26.358 INFO 32268 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-05-23 16:29:26.359 INFO 32268 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-05-23 16:29:26.410 INFO 32268 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-05-23 16:29:27.033 INFO 32268 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2018-05-23 16:29:27.082 INFO 32268 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2018-05-23 16:29:27.085 INFO 32268 --- [ main] c.p.s.ch09.BootExampleApplication : Started BootExampleApplication in 6.068 seconds (JVM running for 7.496)

到目前为止,我们已经准备好了 Spring Boot 应用程序,但我们没有任何要呈现的 URL。因此,当您访问http://localhost:8080时,将显示类似于以下屏幕截图的页面:

让我们定义 Spring 控制器和默认路由,并向其添加文本内容。以下是控制器类的代码片段:

package com.packt.springhighperformance.ch09.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MainController {

  @RequestMapping(value="/")
  @ResponseBody
  public String helloWorld() {
    return "<h1>Hello World<h1>";
  }

  @RequestMapping(value="/welcome")
  @ResponseBody
  public String showMessage(@RequestParam(name="name") String name) {
    return "<h1>Hello " + name + "<h1>";
  }

}

在上面的示例代码中,我们使用@RequestMapping注解定义了两个路由。以下是上述代码块中使用的注解列表及简要描述:

  • @Controller注解表示该类是一个控制器类,可能包含请求映射。

  • @RequestMapping注解定义了用户可以在浏览器中导航到的应用程序 URL。

  • @ResponseBody注解表示方法返回值应该作为 HTML 内容呈现在页面上。value 参数可以采用要导航的 URL 路径。

当我们在浏览器中输入http://localhost:8080时,以下屏幕截图显示了显示或呈现的页面:

我们还定义了带有值/welcome的参数化请求映射。当我们在浏览器中导航到 URL 时,请求参数的值将反映在页面上的消息中。以下屏幕截图显示了内容的呈现方式:

当应用程序使用这些请求映射引导时,我们可以找到以下日志条目:

2018-03-24 10:26:26.154 INFO 11148 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@3c153a1: startup date [Sat Mar 24 10:26:24 IST 2018]; root of context hierarchy
2018-03-24 10:26:26.214 INFO 11148 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/]}" onto public java.lang.String com.packt.springhighperformance.ch09.controllers.MainController.helloWorld()
2018-03-24 10:26:26.218 INFO 11148 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/welcome]}" onto public java.lang.String com.packt.springhighperformance.ch09.controllers.MainController.helloMessage(java.lang.String)

到目前为止,我们的第一个 Spring Boot 应用程序已经有了示例请求映射。本节作为 Spring Boot 应用程序开发的逐步指南。在下一节中,我们将看到更多 Spring Boot 功能。

使用 Spring Boot 执行器的指标

在我们继续之前,了解 Spring Boot 执行器的重要性是很重要的。我们将在接下来的章节中介绍 Spring Boot 执行器。我们还将查看 Spring Boot 执行器提供的开箱即用的功能。我们还将通过示例来了解配置和其他必要的细节。

什么是 Spring 执行器?

实质上,Spring Boot 执行器可以被认为是 Spring Boot 的一个子项目。它可以在我们使用 Spring Boot 开发的应用程序中提供生产级功能。在利用其提供的功能之前,需要配置 Spring Boot 执行器。Spring Boot 执行器自 2014 年 4 月首次发布以来一直可用。Spring Boot 执行器实现了不同的 HTTP 端点,因此开发团队可以执行以下任务:

  • 应用程序监控

  • 分析应用指标

  • 与应用程序交互

  • 版本信息

  • 记录器详情

  • Bean 详情

启用 Spring Boot 执行器

除了帮助引导应用程序开发外,Spring Boot 还可以在应用程序中使用许多功能。这些附加功能包括但不限于监视和管理应用程序。应用程序的管理和监视可以通过 HTTP 端点或使用 JMX 来完成。审计、健康检查和指标也可以通过 Spring Boot 应用程序中的配置来应用。这些都是由spring-boot-actuator模块提供的生产就绪功能。

以下是来自 Spring Boot 参考文档的执行器定义(docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#production-ready):

执行器是一个制造业术语,指的是用于移动或控制某物的机械装置。执行器可以从微小的变化中产生大量运动。

为了利用 Spring Boot Actuator 的功能,第一步是启用它。它不是默认启用的,我们必须添加依赖项才能启用它。在 Spring Boot 应用程序中启用 Spring Boot Actuator 非常容易。如果我们在应用程序中使用 Maven 进行依赖管理,我们需要在 pom.xml 文件中添加 spring-boot-starter-actuator 依赖项。以下是 Maven 依赖项的片段,用于 Spring Boot Actuator:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

如前所述,Spring Boot Actuator 通过暴露或启用端点来实现应用程序监控。该模块具有许多开箱即用的端点。它还允许开发人员创建自定义端点。我们可以启用或禁用每个单独的端点。这确保了端点在应用程序中创建,并且应用程序上下文中存在相应的 bean。

端点可以通过在 JMX 或 HTTP 上暴露来远程访问。通常,应用程序会通过 HTTP 暴露端点。端点的 URL 是通过将端点 ID 与 /actuator 前缀进行映射而派生的。

以下是一些与技术无关的端点列表:

ID 描述 默认启用
auditevents 此端点公开了音频事件的信息。
beans 此端点显示应用程序中可用的所有 Spring beans 的完整列表。
conditions 此端点显示在配置和自动配置类上评估的 conditions
configprops 此端点显示标有 @ConfigurationProperties 的属性列表。
env 此端点显示来自 Spring 的 ConfigurableEnvironment 的属性。
flyway 此端点显示可能已应用的任何 flyway 数据库迁移。
health 此端点显示应用程序的 health 信息。
httptrace 此端点显示 HTTP 跟踪信息。默认情况下,它显示最后 100 个 HTTP 请求-响应交换。
info 此端点公开应用程序信息。
loggers 此端点显示应用程序 logger 配置。
liquibase 此端点显示可能已应用的任何 liquibase 数据库迁移。
metrics 此端点显示应用程序的 metrics 信息。
mappings 此端点显示所有 @RequestMapping 路径的列表。
scheduledtasks 此端点显示应用程序的定时任务。
sessions 此端点允许从 Spring Session 支持的会话存储中检索和删除用户 sessions。在使用 Spring Session 对响应式 Web 应用程序的支持时不可用。
shutdown 此端点允许应用程序优雅地关闭。
threaddump 此端点执行 threaddump

以下是一些在应用程序是 Web 应用程序时暴露的附加端点:

ID 描述 默认启用
heapdump 此端点返回一个压缩的 hprof 堆转储文件。
jolokia 此端点通过 HTTP 公开 JMX bean。
logfile 如果在属性中设置了 logging.filelogging.path,此端点将显示 logfile 的内容。它使用 HTTP 范围标头来部分检索日志文件的内容。
prometheus 此端点显示以 Prometheus 服务器可以抓取的格式的指标。

启用端点

使用 Spring Boot Actuator,默认情况下所有端点都是启用的,除了 shutdown 端点。为了启用或禁用特定端点,应在 application.properties 文件中添加相关属性。以下是启用端点的格式:

management.endpoint.<id>.enabled=true

例如,可以添加以下属性以启用shutdown端点:

management.endpoint.shutdown.enabled=true

当我们启动一个默认启用 Actuator 端点的应用程序时,可以看到以下日志条目:

2018-03-24 17:51:36.687 INFO 8516 --- [ main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator/health],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
2018-03-24 17:51:36.696 INFO 8516 --- [ main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator/info],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
2018-03-24 17:51:36.697 INFO 8516 --- [ main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto protected java.util.Map<java.lang.String, java.util.Map<java.lang.String, org.springframework.boot.actuate.endpoint.web.Link>> org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping.links(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)

仔细查看日志条目,我们发现以下端点或 URL 被暴露:

  • /actuator

  • /actuator/health

  • /actuator/info

应用程序为什么有三个端点暴露出来,而之前列出的端点如此之多?为了回答这个问题,Spring Boot Actuator 只在 HTTP 上暴露了三个端点。之前列出的其余端点是通过 JMX 连接暴露的。以下是端点列表以及它们是否在 HTTP 或 JMX 上暴露的信息:

ID 在 JMX 上暴露 在 HTTP 上暴露
auditevents
beans
conditions
configprops
env
flyway
health
heapdump N/A
httptrace
info
jolokia N/A
logfile N/A
loggers
liquibase
metrics
mappings
prometheus N/A
scheduledtasks
sessions
shutdown
threaddump

Spring Boot 为什么不默认在 HTTP 上暴露所有端点?原因是端点可能暴露敏感信息。因此,在暴露它们时应该仔细考虑。

以下属性可用于更改或覆盖端点的默认暴露行为:

  • management.endpoints.jmx.exposure.exclude: 以逗号分隔的端点 ID 从默认的 JMX 连接暴露中排除。默认情况下,没有一个默认端点被排除。

  • management.endpoints.jmx.exposure.include: 以逗号分隔的端点 ID 与默认的 JMX 连接暴露一起包括。该属性可用于暴露那些未包含在默认端点列表中的端点。该属性的默认值是*,表示所有端点都被暴露。

  • management.endpoints.web.exposure.exclude: 以逗号分隔的端点 ID 从 HTTP 暴露中排除。虽然没有默认值,但只有infohealth端点被暴露。其余端点对于 HTTP 隐式排除。

  • management.endpoints.web.exposure.include: 以逗号分隔的端点 ID 包括在默认的 HTTP 暴露中。该属性可用于暴露那些未包含在默认端点列表中的端点。该属性的默认值是infohealth

健康检查

确保应用程序高性能的一个极其关键的方面是监控应用程序的健康状况。生产级应用程序始终受到专门监控和警报软件的监视。为每个参数配置了阈值,无论是平均响应时间、磁盘利用率还是 CPU 利用率。一旦参数值超过指定的阈值,监控软件通过电子邮件或通知发出警报。开发和运维团队采取必要的措施,确保应用程序恢复到正常状态。

对于 Spring Boot 应用程序,我们可以通过导航到/actuator/health URL 来收集健康信息。health端点默认启用。对于部署在生产环境中的应用程序,使用health端点收集的健康信息可以发送到监控软件进行警报目的。

health端点呈现的信息取决于management.endpoint.health.show-details属性。以下是该属性支持的值列表:

  • always:表示所有信息都应显示给所有用户。

  • never:表示永远不显示详细信息。

  • when-authorized:表示只有授权角色的用户才能查看详细信息。授权角色可以使用management.endpoint.health.roles属性进行配置。

show-details属性的默认值为never。此外,当用户具有一个或多个端点的授权角色时,用户可以被视为已授权。默认情况下,没有角色被配置为已授权。因此,所有经过身份验证的用户都被视为已授权用户。

HealthIndicator是一个重要的接口,它提供了关于应用程序健康状况的指示,例如磁盘空间、数据源或 JMS。health端点从应用程序上下文中定义的所有HealthIndicator实现 bean 收集健康信息。Spring Boot 带有一组自动配置的健康指标。该框架足够灵活,可以支持自定义健康指标的实现。应用程序的最终健康状态由HealthAggregator派生。健康聚合器根据已定义的状态顺序对所有健康指标的状态进行排序。

以下是 Spring Boot 自动配置的HealthIndicators列表:

  • CassandraHealthIndicator:检查 Cassandra 数据库是否正常运行

  • DiskSpaceHealthIndicator:检查是否有足够的磁盘空间可用

  • DataSourceHealthIndicator:检查是否可以与数据源建立连接

  • ElasticSearchHealthIndicator:检查 elasticsearch 集群是否正常

  • InfluxDbHealthIndicator:检查 Influx 服务器是否正常运行

  • JmsHealthIndicator:检查 JMS 代理是否正常运行

  • MailHealthIndicator:检查邮件服务器是否正常运行

  • MongoHealthIndicator:检查 Mongo 数据库是否正常运行

  • Neo4jHealthIndicator:检查 Neo4j 服务器是否正常运行

  • RabbitHealthIndicator:检查 Rabbit 服务器是否正常运行

  • RedisHealthIndicator:检查 Redis 服务器是否正常运行

  • SolrHealthIndicator:检查 Solr 服务器是否正常运行

这些健康指标是基于适当的 Spring Boot starter 配置进行自动配置的。

当我们导航到http://localhost:8080/actuator/health URL 时,以下是示例磁盘空间健康检查的输出:

{
  "status": "UP",
  "details": {
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 407250137088,
        "free": 392089661440,
        "threshold": 10485760
      }
    }
  }
}

我们可以添加额外的自定义健康指标来包含我们想要查看的信息。自定义健康指标将显示在health端点的结果中。创建和注册自定义健康指标非常容易。

以下是自定义健康指标的示例:

package com.packt.springhighperformance.ch09.healthindicators;

import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.stereotype.Component;

@Component
public class ExampleHealthCheck extends AbstractHealthIndicator {
    @Override
      protected void doHealthCheck(Health.Builder builder) 
      throws Exception   
   {
        // TODO implement some check
        boolean running = true;
        if (running) {
          builder.up();
        } else {
          builder.down();
        }
    }
}

我们必须创建一个 Java 类,该类继承自AbstractHealthIndicator。在自定义健康指标类中,我们必须实现doHealthCheck()方法。该方法期望传递一个Health.Builder对象。如果我们发现健康参数正常,则应调用builder.up()方法,否则应调用builder.down()方法。

当访问/actuator/health URL 时,以下是页面上呈现的输出:

{
  "status": "UP",
  "details": {
    "exampleHealthCheck": {
 "status": "UP"
 },
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 407250137088,
        "free": 392071581696,
        "threshold": 10485760
      }
    },
    "db": {
      "status": "UP",
      "details": {
        "database": "MySQL",
        "hello": 1
      }
    }
  }
}

不需要注册自定义健康指标。@Component注解会被扫描,并且该 bean 会被注册到ApplicationContext中。

到目前为止,我们已经详细学习了 Spring Boot 并举例说明。接下来的部分将专注于使用 Spring Boot 与微服务。

使用 Spring Boot 的微服务

我们现在已经从前面的部分中获得了大量关于 Spring Boot 的信息。有了我们到目前为止所拥有的信息,我们现在有能力使用 Spring Boot 构建微服务。在着手实现我们的第一个 Spring Boot 微服务之前,假设您已经了解了关于微服务的基本信息,包括单体应用程序的问题、微服务的定义以及微服务带来的特性。

使用 Spring Boot 的第一个微服务

以下是我们将要开发的微服务的详细信息:

  • 我们将实现一个作为微服务的会计服务。

  • 这个微服务将是基于 REST 的。这是一种用于开发 Web 服务的架构模式。它专注于使用唯一的 URL 标识应用程序中的每个资源。

  • 我们将确定我们需要的 Spring Boot 启动器项目,并相应地生成 Maven 的pom.xml文件。

  • 我们将实现一个带有一些基本属性的Account类。

  • 我们将使用 find-by-name 示例方法实现AccountRepository

  • 我们将实现控制器类,其中有一个自动装配的存储库。控制器公开了端点。

  • 我们还将实现一种将测试数据输入到数据库的方法。

让我们开始吧!

我们将通过使用 Spring Initializr 生成 Spring Boot 应用程序来开始实现。我们必须决定要使用的 Spring Boot 启动项目。我们想要开发一个基于 JPA 的 Web 应用程序。为了在数据库中存储Account数据,我们可以使用 MySQL 或 H2。通常,H2 是一个更方便的选择,因为我们不需要设置任何东西。在本章的示例中,我们将使用 MySQL。

以下是要选择的启动项目:

  • Web

  • JPA

  • MySQL 或 H2

  • REST 存储库

我们还可以添加 Spring Boot Actuator 进行应用程序监控,但这对于示例来说并不是必需的。

以下是 Spring Initializr 生成的pom.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<project  
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
  http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.packt.springhighperformance.ch09</groupId>
  <artifactId>ch-09-accounting-service</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>accounting-service</name>
  <description>Example accounting service</description>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.RELEASE</version>
    <relativePath /> <!-- lookup parent from repository -->
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-    
    8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-rest</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-hateoas</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-rest-hal-browser</artifactId>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

Spring Initializr 生成的另一段代码是 Spring Boot 应用程序:

package com.packt.springhighperformance.ch09.accountingservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AccountingServiceApplication {

  public static void main(String[] args) {
    SpringApplication.run(AccountingServiceApplication.class, args);
  }
}

到目前为止,我们应该已经将我们的项目导入到我们首选的 IDE 中。

人们,准备好进行实际开发了。我们将从创建Account JPA 实体类开始。我们将使用@Entity@Table注解来注释Account类。@Table注解允许我们提供所需的表名。我们还有一个列,即accountName。它存储并表示Account的名称。基本上,Account实体代表了现实世界中的账户类型。我们添加的另一个重要属性是idid代表一个唯一的、自动生成的数字标识符。我们可以使用这个标识符唯一地标识每个账户。@GeneratedValue注解允许我们提供在数据库中生成id值的方式。将其保持为AUTO定义了它取决于数据库自动生成id值。@Column注解允许我们将accountName属性与ACCT_NAME数据库字段匹配。

以下是Account实体的代码:

package com.packt.springhighperformance.ch09.accountingservice.models;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "accounts")
public class Account {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "ACCT_ID")
  private Long id;

  @Column(name = "ACCT_NAME")
      private String accountName;

  public Account() {
  }

  public Account(String accountName) {
    this.accountName = accountName;
  }

  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getAccountName() {
    return accountName;
  }

  public void setAccountName(String accountName) {
    this.accountName = accountName;
  }

  @Override
  public String toString() {
    return "Account{"
        + "id=" + id + 
        ", accountName='" + accountName + '\'' +
        '}';
  }

}

Spring Data 提供了一个方便的接口来执行常见的数据库操作。这个接口叫做CrudRepository。它支持特定类型的基本CreateReadUpdateDelete操作。这个接口是由JpaRepository接口继承的,它是CrudRepository接口的 JPA 特定定义。JpaRepository还从PagingAndSortingRepository接口继承了排序和分页功能。

有了这个背景,我们接下来的任务是构建一个与accounts数据库表交互的接口。以下是AccountsRepository类的代码:

package com.packt.springhighperformance.ch09.
accountingservice.repositories;

import java.util.Collection;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

import com.packt.springhighperformance.ch09.accountingservice.models.Account;

@RepositoryRestResource
public interface AccountsRepository extends JpaRepository<Account, Long> {

  Collection<Account> findByAccountName(@Param("an") String an);
}

AccountsRepository接口中,我们定义了一个方法,用于根据accountName从数据库中查找Account条目。CrudRepository接口非常强大。它将为findByAccountName方法生成实现。它可以为所有遵循约定的查询方法生成实现,例如findBy{model-attribute-name}。它还返回Account类型的对象。

另外,你可能已经注意到,@RepositoryRestResource的使用是由 Spring Data REST 模块提供的。它简要地将存储库方法暴露为 REST 端点,无需进一步配置或开发。

现在,我们已经有了实体和存储库。接下来是 Web 应用程序的控制器部分。我们需要创建一个控制器类。以下是AccountsController类的代码:

package com.packt.springhighperformance.ch09
.accountingservice.controllers;

import java.util.Collections;
import java.util.Map;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AccountsController {
  @GetMapping(value = "/account/{name}")
  Map<String, Object> getAccount(@PathVariable String name) {
    return Collections.singletonMap("Account : ", name);
  }
}

AccountsController代码中的三个值得注意的注解是:

  • @RestController:这个注解是@Controller@ResponseBody注解的组合。如果我们使用@RestController注解,就不需要定义这两个其他的注解。@RestController注解表示该类应该被视为一个控制器,每个端点方法都会作为响应体返回内容。

  • @GetMapping:这个注解用于定义 REST GET端点映射。

  • @PathVariable:这个注解用于获取 URL 路径中提供的值。

还有两件事情。一是数据库和其他重要属性,另一个是在accounts表中填充初始数据的方式。

以下是管理应用程序配置部分的application.properties文件:

spring.jpa.hibernate.ddl-auto=create-drop
spring.datasource.url=jdbc:mysql://localhost:3306/db_example?useSSL=false
spring.datasource.username=root
spring.datasource.password=root

从属性列表中,spring.jpa.hibernate.ddl-auto属性确定了基于提供的数据库配置的数据库的初始生成。它确定了 Spring Boot 应用程序是否应该在应用程序启动时创建数据库模式。nonevalidateupdatecreatecreate-drop是该属性的可用选项。

在启动应用程序时,我们可能还会收到以下错误:

Establishing SSL connection without server's identity verification is not recommended.

我们可以在数据库连接 URL 中使用useSSL=true来解决这个警告,就像你在前面的代码示例中看到的那样。

向数据库加载示例数据

此时,有必要在数据库的accounts表中有一些初始数据。这将帮助我们测试我们开发的账户微服务。Spring 模块提供了多种方法来实现这一点。

JPA 的初始数据加载方式

Spring Data JPA 提供了一种在应用程序启动时执行数据库操作命令的方式。由于数据库模式将根据 JPA 实体配置和ddl-auto属性值在数据库中生成,我们必须注意只在accounts表中插入账户记录。以下是实现这一点的步骤:

  1. application.properties文件中添加以下属性:
spring.datasource.initialization-mode=always
  1. 在项目的src/main/resources文件夹中创建一个data.sql文件,其中包含INSERT查询:
INSERT INTO accounts (ACCT_NAME) VALUES
  ('Savings'),
  ('Current'),
  ('Fixed Deposit'),
  ('Recurring Deposit'),
  ('Loan');

就是这样!当我们启动应用程序时,Spring 会自动将数据插入到数据库的accounts表中。

ApplicationRunner 的初始数据加载方式

我们也可以使用ApplicationRunner接口来实现这一点。这个接口负责在应用启动时执行run方法中定义的代码。

以下是ApplicationRunner接口实现的代码:

package com.packt.springhighperformance.ch09.accountingservice;

import java.util.stream.Stream;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import com.packt.springhighperformance.ch09.accountingservice.models.Account;
import com.packt.springhighperformance.ch09.accountingservice.repositories.AccountsRepository;

@Component
public class AccountsDataRunner implements ApplicationRunner {

  @Autowired
  private AccountsRepository acctRepository;

  @Override
  public void run(ApplicationArguments args) throws Exception {
    Stream.of("Savings", "Current", "Recurring", "Fixed Deposit")
    .forEach(name -> acctRepository.save(new Account(name)));
    acctRepository.findAll().forEach(System.out::println);
  }

}

我们已经自动装配了存储库,这样我们就可以访问AccountsRepository方法,将accounts记录插入到数据库中。

微服务客户端

现在我们已经有了微服务,我们必须看看如何消费它。计划是使用 Spring Initializr 创建另一个 Web 应用程序,并使用适当的工具来消费会计微服务。

以下是客户端应用程序的 POM 文件:

<?xml version="1.0" encoding="UTF-8"?>
<project  
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
  http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.packt.springhighperformance.ch09</groupId>
  <artifactId>ch-09-accounting-service-client</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>accounting-service-client</name>
  <description>Example accounting service client</description>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.RELEASE</version>
    <relativePath /> <!-- lookup parent from repository -->
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-
    8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <spring-cloud.version>Finchley.M9</spring-cloud.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-openfeign</artifactId>
 </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <dependencyManagement>
 <dependencies>
 <dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-dependencies</artifactId>
 <version>${spring-cloud.version}</version>
 <type>pom</type>
 <scope>import</scope>
 </dependency>
 </dependencies>
 </dependencyManagement>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

  <repositories>
    <repository>
      <id>spring-milestones</id>
      <name>Spring Milestones</name>
      <url>https://repo.spring.io/milestone</url>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
    </repository>
  </repositories>

</project>

在上述的pom.xml文件中,我们使用 Maven 的 dependency-management 元素导入了 Spring Cloud 依赖项。我们还添加了openfeign starter 项目。Feign 是一个用于消费 Web 服务并提供 REST 客户端模板设施的客户端工具。

以下是我们 Spring Boot 客户端应用程序中main类的代码:

package com.packt.springhighperformance.ch09.accountingclient;

import java.util.Map;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.json.BasicJsonParser;
import org.springframework.boot.json.JsonParser;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class AccountingServiceClientApplication {

  public static void main(String[] args) {
    SpringApplication.run(AccountingServiceClientApplication.class, 
    args);
  }
}

@RestController
class MainController {

  @Value("${accounting.service.url}")
  private String accountingServiceUrl;

  @GetMapping("/account")
  public String getAccountName(@RequestParam("id") Long id) {
    ResponseEntity<String> responseEntity = new 
    RestTemplate().getForEntity(accountingServiceUrl + "/" + id,
    String.class);
    JsonParser parser = new BasicJsonParser();
    Map<String, Object> responseMap = 
    parser.parseMap(responseEntity.getBody());
    return (String) responseMap.get("accountName");
  }
}

我们在同一个 Java 文件中定义了 REST 控制器。

以下是定义微服务 URL 并定义运行客户端应用程序的server.portapplication.properties文件:

accounting.service.url=http://localhost:8080/accounts/
server.port=8181

使用 Spring Cloud 的微服务

Spring Cloud 提供了一种声明式的方法来构建云原生 Web 应用程序。云原生是一种应用程序开发范式,鼓励采用价值驱动的开发最佳实践。Spring Cloud 是建立在 Spring Boot 之上的。Spring Cloud 为分布式系统中的所有组件提供了易于访问所有功能的方式。

Spring Cloud 提供:

  • 由 Git 管理的集中式配置数据的版本控制

  • 与 Netflix Eureka 和 Ribbon 配对,以便应用程序服务动态发现彼此

  • 将负载均衡决策从专用代理负载均衡器推送到客户端服务

外部化配置是 Spring Cloud 的主要优势之一。在下一节中,我们将开发一个示例来展示 Spring Boot 应用程序的外部化配置。

Spring 微服务配置示例

为了使外部化配置生效,我们需要设置一个集中式配置服务器。配置服务器将存储并提供注册的 Spring Boot 应用程序的配置数据。在本节中,我们将开发一个配置服务器,之前开发的会计服务将作为配置客户端。

以下是 Spring Boot 配置服务器的 POM 文件:

<?xml version="1.0" encoding="UTF-8"?>
<project      

    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0     
    http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.spring.server.config</groupId>
  <artifactId>spring-config-server</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>config-server</name>
  <description>Example spring boot config server</description>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.RELEASE</version>
    <relativePath /> <!-- lookup parent from repository -->
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-
    8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <spring-cloud.version>Finchley.M9</spring-cloud.version>
  </properties>

  <dependencies>
    <dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-config-server</artifactId>
 </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <dependencyManagement>
 <dependencies>
 <dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-dependencies</artifactId>
 <version>${spring-cloud.version}</version>
 <type>pom</type>
 <scope>import</scope>
 </dependency>
 </dependencies>
 </dependencyManagement>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

  <repositories>
    <repository>
      <id>spring-milestones</id>
      <name>Spring Milestones</name>
      <url>https://repo.spring.io/milestone</url>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
    </repository>
  </repositories>
</project>

应该注意前面的依赖项中的两个配置:

  • spring-cloud-dependencies它提供了 Spring Cloud 项目所需的一组依赖项

  • spring-cloud-config-server这是 Spring Boot 的 Spring Cloud starter 项目

以下是application.properties文件:

spring.application.name=configserver
spring.cloud.config.server.git.uri:${user.home}\\Desktop\\config-repo
server.port=9000
spring.profiles.active=development,production

spring.cloud.config.server.git.uri属性指向存储配置的基于 Git 的目录。版本控制由 Git 本身维护。

spring.profiles.active表示应用程序要使用的配置文件。对于开发团队来说,拥有多个环境是一个常见的用例。为了为每个环境设置单独的配置,我们可以使用这个属性。

@EnableConfigServer注解由 Spring Cloud starter 项目提供。它标记类为配置服务器。以下是 Spring Boot 应用程序main类的代码:

package com.spring.server.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {

  public static void main(String[] args) {
    SpringApplication.run(ConfigServerApplication.class, args);
  }
}

完成后,配置服务器准备就绪。在 Git 存储库中,我们已经创建了一个名为accountingservice.properties的文件,内容如下:

server.port=8101

应用程序启动后,我们可以导航到http://localhost:9000/accountingservice/default。由于配置服务器中没有accountingservice应用程序的特定配置文件,它会选择默认配置。页面的内容如下所示:

正如我们所看到的,server.port属性值在页面上呈现。

下一步是构建一个客户端,利用配置服务器中定义的集中式配置。我们必须创建一个带有 web 依赖的 Spring Boot starter 应用程序。

以下是配置服务器客户端的 POM 文件:

<?xml version="1.0" encoding="UTF-8"?>
<project  

    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0     
    http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.packt.springhighperformance.ch09</groupId>
  <artifactId>ch-09-accounting-service</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>accounting-service</name>
  <description>Example accounting service</description>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.RELEASE</version>
    <relativePath /> <!-- lookup parent from repository -->
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-
    8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-config</artifactId>
 <version>2.0.0.M9</version>
 </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

正如我们在前面的 Maven 文件中所看到的,我们需要将spring-cloud-config-starter项目添加为依赖项。该项目为应用程序注册为配置服务器客户端提供了必要的配置。

以下是application.properties文件:

management.endpoints.web.exposure.include=*
server.port=8888

为了将应用程序注册为配置服务器的客户端,我们必须启用管理 Web 端点。服务器将在端口8888上运行,根据application.properties文件中的配置。

Spring Cloud 在另一个上下文中运行,称为bootstrap上下文。引导上下文是主ApplicationContext的父级。引导上下文的责任是将外部配置属性从外部源加载到本地外部配置中。建议为引导上下文单独创建一个属性文件。

以下是bootstrap.properties文件中的属性:

spring.application.name=accountingservice
spring.cloud.config.uri=http://localhost:9000

我们已经定义了与配置属性文件在配置服务器的 Git 目录中存储的名称匹配的应用程序名称。bootstrap.properties文件还定义了 Spring Cloud 配置服务器的 URL。

这就是客户端注册到 Spring Cloud 配置服务器的全部内容。在服务器启动时可以看到以下日志条目:

2018-04-01 16:11:11.196 INFO 13556 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at: http://localhost:9000
....

2018-04-01 16:11:13.303  INFO 13556 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8101 (http)
....

2018-04-01 16:11:17.825  INFO 13556 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8101 (http) with context path ''

正如您所看到的,尽管我们已经为客户端应用程序定义了服务器端口为8888,但它从配置服务器获取server.port属性,并在端口8101上启动 Tomcat。当我们渲染/accounts URL 时,页面看起来像这样:

本节逐步介绍了创建简单配置服务器和使用配置服务器的客户端的方法。在接下来的部分中,我们将看到一种监视 Spring 微服务的方法。

使用 Spring Boot admin 监视微服务

Spring Boot admin 是一个便于监视和管理 Spring Boot 应用程序的应用程序。Spring Boot admin 应用程序的最新版本尚不兼容 Spring 2.0.0。在本节展示的示例中,我们使用了 Spring Boot 1.5.11 快照。Spring Boot admin 版本为 1.5.4。

Spring Boot 客户端应用程序通过 HTTP 向 Spring Boot 管理应用程序注册自己。管理应用程序还可以使用 Spring Cloud Eureka 发现服务发现客户端应用程序。Spring Boot 管理用户界面是在 AngularJS 上构建的,覆盖了执行器端点。

这应该足够作为介绍部分,示例将提供更多见解。让我们首先构建 Spring Boot 管理服务器。

spring-boot-admin-server是构建管理服务器应用程序的依赖项。Spring Boot 管理应用程序可以注册多个 Spring Boot 应用程序,因此,Spring Boot 管理应用程序必须是安全的。这就是我们添加 Spring Security starter 项目依赖项的原因。我们将为此应用程序添加基本身份验证,但这并不是限制。我们可以添加高级安全机制,如 OAuth,以保护应用程序。以下是 Spring Boot 管理服务器的 POM 文件:

<?xml version="1.0" encoding="UTF-8"?>
<project  

    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
    http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.spring.admin</groupId>
  <artifactId>admin-server</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>admin-server</name>
  <description>Demo project for Spring Boot</description>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.11.BUILD-SNAPSHOT</version>
    <relativePath /> <!-- lookup parent from repository -->
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
 <groupId>de.codecentric</groupId>
 <artifactId>spring-boot-admin-server</artifactId>
 <version>1.5.4</version>
 </dependency>
 <dependency>
 <groupId>de.codecentric</groupId>
 <artifactId>spring-boot-admin-server-ui</artifactId>
 <version>1.5.4</version>
 </dependency>
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-security</artifactId>
 </dependency>
 <dependency>
 <groupId>de.codecentric</groupId>
 <artifactId>spring-boot-admin-server-ui-login</artifactId>
 <version>1.5.4</version>
 </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

  <repositories>
    <repository>
      <id>spring-snapshots</id>
      <name>Spring Snapshots</name>
      <url>https://repo.spring.io/snapshot</url>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
    </repository>
    <repository>
      <id>spring-milestones</id>
      <name>Spring Milestones</name>
      <url>https://repo.spring.io/milestone</url>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
    </repository>
  </repositories>

  <pluginRepositories>
    <pluginRepository>
      <id>spring-snapshots</id>
      <name>Spring Snapshots</name>
      <url>https://repo.spring.io/snapshot</url>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
    </pluginRepository>
    <pluginRepository>
      <id>spring-milestones</id>
      <name>Spring Milestones</name>
      <url>https://repo.spring.io/milestone</url>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
    </pluginRepository>
  </pluginRepositories>
</project>

application.properties文件是我们定义访问管理应用程序的安全凭据的地方。以下是application.properties文件的内容:

security.user.name=admin
security.user.password=admin

@EnableAdminServer由 Spring Boot admin 服务器依赖项提供。它表示应用程序作为 Spring Boot admin 应用程序运行。以下是 Spring Boot 应用程序main类的代码:

package com.spring.admin.adminserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import de.codecentric.boot.admin.config.EnableAdminServer;

@SpringBootApplication
@EnableAdminServer
public class AdminServerApplication {

  public static void main(String[] args) {
    SpringApplication.run(AdminServerApplication.class, args);
  }
}

下一步是构建一个样本应用程序,该应用程序将注册到 Spring Boot 管理应用程序。以下是 POM 文件:

<?xml version="1.0" encoding="UTF-8"?>
<project  

    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
    http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <parent>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-parent</artifactId>
 <version>1.5.11.BUILD-SNAPSHOT</version>
 <relativePath /> <!-- lookup parent from repository -->
 </parent>

  <properties>
    <spring-boot-admin.version>1.5.7</spring-boot-admin.version>
  </properties>

  <dependencies>
    <dependency>
 <groupId>de.codecentric</groupId>
 <artifactId>spring-boot-admin-starter-client</artifactId>
 </dependency>
    <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-actuator</artifactId>
 </dependency>
    <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-security</artifactId>
 </dependency>
</project>

我们必须定义以下属性:

  • spring.boot.admin.url:该 URL 指向 Spring Boot 管理应用程序。

  • spring.boot.admin.username:管理客户端需要使用安全凭据访问管理应用程序。此属性指定了管理应用程序的用户名。

  • spring.boot.admin.password:此属性指定了管理应用程序的密码。

  • management.security.enabled:此属性表示客户端应用程序是否启用了安全性。

  • security.user.name:此属性定义了访问客户端应用程序的用户名。

  • security.user.password:此属性指定了访问客户端应用程序的密码。

以下是application.properties文件:

spring.boot.admin.url=http://localhost:8080
server.port=8181
spring.boot.admin.username=admin
spring.boot.admin.password=admin
management.endpoints.web.exposure.include=*
security.user.name=user
security.user.password=password
management.security.enabled=false

以下是简单 Spring Boot 应用程序类的代码:

package com.spring.admin.client.bootadminclient;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BootAdminClientApplication {

  public static void main(String[] args) {
    SpringApplication.run(BootAdminClientApplication.class, args);
  }
}

还可以对 Spring Security 提供的默认 Web 安全配置进行自定义。以下是一个示例,演示了允许所有请求进行授权的情况:

package com.spring.admin.client.bootadminclient;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class SecurityPermitAllConfig extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().anyRequest().permitAll().
    and().csrf().disable();
  }
}

此时,我们准备启动 Spring Boot 管理和客户端应用程序。当我们导航到 Spring Boot 管理应用程序的 URL 时,将显示以下屏幕,其中列出了所有注册的应用程序:

单击应用程序名称右侧的“详细信息”按钮将显示类似于此处所示的界面。详细信息选项卡显示应用程序的健康状况、内存和 JVM 统计信息以及垃圾收集器详细信息:

应用程序详细信息的日志选项卡显示了所有配置的记录器列表。可以更改日志级别。以下是日志的界面:

这就是 Spring Boot 管理应用程序的全部内容。它提供了用于监视 Spring Boot 应用程序的生产级界面和详细信息。下一节将提供 Spring Boot 应用程序的性能调优。

Spring Boot 性能调优

Spring Boot 是一个很好的工具,可以快速启动和开发基于 Spring Framework 的应用程序。毫无疑问,Spring Boot 应用程序的原始版本提供了高性能。但随着应用程序的增长,其性能开始成为瓶颈。这对所有 Web 应用程序来说都是正常情况。当添加不同的功能并且每天增加的请求时,就会观察到性能下降。在本节中,我们将学习 Spring Boot 应用程序的性能优化技术。

Undertow 作为嵌入式服务器

Spring Boot 提供了可以在 JAR 文件中运行 Web 应用程序的嵌入式服务器。可用于使用的一些嵌入式服务器包括 Tomcat、Undertow、Webflux 和 Jetty。建议使用 Undertow 作为嵌入式服务器。与 Tomcat 和 Jetty 相比,Undertow 提供了更高的吞吐量并且消耗的内存更少。以下比较可能会提供一些见解:

  • 吞吐量比较:
服务器 样本 错误% 吞吐量
Tomcat 3000 0 293.86
Jetty 3000 0 291.52
Undertow 3000 0 295.68
  • 堆内存比较:
服务器 堆大小 已使用 最大
Tomcat 665.5 MB 118.50 MB 2 GB
Jetty 599.5 MB 297 MB 2 GB
Undertow 602 MB 109 MB 2 GB
  • 线程比较:
服务器 活动 已启动
Tomcat 17 22
Jetty 19 22
Undertow 17 20

从前面的比较中,Undertow 看起来是 Spring Boot 应用程序中嵌入式服务器的明显选择。

使用@SpringBootApplication 注解的开销

@SpringBootApplication注解是为那些习惯于使用@ComponentScan@EnableAutoConfiguration@Configuration注解 Spring 类的开发人员提供的。因此,@SpringBootApplication注解相当于使用三个带有默认配置的注解。隐式的@ComponentScan注解扫描在基本包(Spring Boot 应用程序主类的包)和所有子包中定义的 Java 类。当应用程序在规模上显著增长时,这会减慢应用程序的启动速度。

为了克服这一点,我们可以用单独的注解替换@SpringBootApplication注解,其中我们提供要与@ComponentScan一起扫描的包路径。我们还可以考虑使用@Import注解来仅导入所需的组件、bean 或配置。

摘要

本章以对 Spring Boot、Spring Cloud、微服务以及它们的综合详细信息开始。我们涵盖了 Spring Initializr 的细节,Spring Boot starter 项目,并学习了如何创建我们的第一个 Spring Boot 应用程序。然后,我们了解了 Spring Boot 执行器和执行器提供的生产级功能。应用程序健康检查和端点的细节对于生产就绪的应用程序非常重要。

在本章的后面,我们迁移到了微服务的世界。我们学习了 Spring Boot 如何利用功能来构建微服务。我们使用 Spring Boot 和 Spring Cloud 开发了一个支持外部化配置的微服务。我们还研究了 Spring Boot 管理器集成,用于监控 Spring Boot 应用程序。最后但同样重要的是,我们学习了一些提高 Spring Boot 应用程序性能的技术。相当庞大的内容,不是吗?

到目前为止,您对 Spring 和基本上任何基于 Java 的 Web 应用程序的性能评估和性能调优有很好的理解。这就是本书的范围。在向前迈进一步时,您可以学习 JVM 类加载机制、Spring Batch 框架、微服务设计模式、微服务部署和基础设施即服务(IaaS)。我们希望您会发现这些内容有帮助。

posted @ 2024-05-24 10:54  绝不原创的飞龙  阅读(42)  评论(0编辑  收藏  举报