Spring-安卓应用开发学习指南-全-

Spring 安卓应用开发学习指南(全)

原文:zh.annas-archive.org/md5/2ad6f3c074671894140b1a76c2d8a4ad

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书旨在开发应用程序的服务器端和客户端。我们为服务器端和客户端都使用了 Kotlin 语言。在本书中,Spring 将作为服务器端应用,Android 作为客户端应用。我们的主要重点是那些能够帮助开发者使用最新架构开发安全应用的区域。本书描述了 Kotlin 和 Spring 的基础知识,如果你对这些平台不熟悉,这将很有益。我们还设计了关于在项目中实现安全和数据库的章节。本书深入探讨了 Retrofit 在处理 HTTP 请求中的应用,以及 SQLite Room 在 Android 设备中存储数据。你还将找到一种开发健壮、响应式项目的方法。然后,你将学习如何使用 JUnit 和 Espresso 测试项目,以开发一个更少错误和更稳定的工程。

本书面向对象

本书是为那些希望使用 Spring 和 Android 开发项目的 Kotlin 新手开发者设计的。Spring for Android 提供了一个功能性的 REST 客户端,支持从 JSON 中序列化对象。开发者依赖于其他语言平台,如 PHP 和 Python 进行 REST API 开发,但 Spring 与 Java/Kotlin 结合,并提供了丰富的内容,帮助开发者以最大安全性使用 REST API。应用程序代码中存在一些依赖项,Spring 会移除这些依赖项。如今,Java 正在被 Kotlin 取代,Kotlin 更轻量级,需要更少的代码行来完成工作。

本书涵盖内容

第一章,关于环境,为服务器端和客户端创建了一个环境。我们还将探讨项目所需的各种工具类型。我们将了解使用 Spring 和 Android 平台可以创建什么。

第二章,Kotlin 概述,涵盖了 Kotlin 的基础知识,并探讨了如何设置环境以及 Kotlin 可用的工具或 IDE,包括基本语法和类型。我们将看到流程结构,包括 if-else 语句、for 循环和 while 循环。我们还将探讨 Kotlin 的面向对象编程,包括类、接口、对象等。函数也将被涵盖,包括参数、构造函数和语法。我们还将解释空安全、反射和注解,这些都是 Kotlin 的核心特性。

第三章,Spring 框架概述,涵盖了 Spring 框架的基础知识,读者将学习如何配置 Spring 和 bean。本章将解释依赖注入,以及 Spring 的架构。读者将了解 Spring MVC 和 Spring Boot,这些对于快速开发应用程序非常有帮助。还将解释 Spring 数据模块。我们还将介绍 Spring Security,它为应用程序提供身份验证和其他安全功能。

第四章,Android 的 Spring 模块,涵盖了与 Android 项目相关的 RestTemplate 和 Retrofit 模块。提供了 HTTP 客户端的解释。还将涵盖对象到 JSON 的序列化。我们将学习如何启动和设置环境。RestTemplate 和 Retrofit 模块的 HTTP 请求方法,如POSTGETUPDATEDELETE,以及其他 Spring 模块的常见功能,以及 Maven 依赖管理也将被介绍。

第五章,使用 Spring Security 保护应用程序,涵盖了 Spring Security 的要求。我们将学习如何在 Web 服务器中注册和配置安全和身份验证。我们还将了解 Spring Security 的架构以及如何将其用于客户端。我们将看到为 Android 应用程序保护 API 的方法以及安全流程。我们将学习如何与 REST API 相关联使用 Spring Security。还将讨论基本身份验证、OAuth2、隐式流和授权代码流的使用。我们还将学习如何与 Android 项目连接并使用基本身份验证。

第六章,访问数据库,涵盖了现有的 Spring 数据模块。我们还将介绍 JDBC、JPA、H2、MySQL for Spring 以及 Android 的 SQLite Room。我们还将学习如何使用 JPA 在 Spring 中创建 REST API 以及如何在 Android 中获取 API 和处理内容。

第七章,并发,涵盖了协程,包括并发、并行和线程池等内容。我们还将学习关于顺序操作和回调地狱的知识。

第八章,响应式编程,涵盖了响应式编程相关主题,包括 Spring Reactor 和阻塞。在本章中,读者还将学习 RxJava 和 RxAndroid。

第九章 Creating an Application,创建应用程序,从安装 Android 环境开始。然后,我们将在 Web 服务器上配置 Spring 并设计项目。然后,我们将创建 UI、布局和 RESTful Web 服务,并从 API 中检索 JSON。我们还将学习使用 Spring Boot 和 Spring Security 为应用程序提供支持。然后,我们将学习如何使用基本认证来保护数据并允许用户访问。我们将使用安全的 REST API 为 Android 应用程序提供支持,以及如何在 Android 中处理内容。此应用程序将基于 Kotlin,我们将利用 Kotlin 的功能,包括空安全、反射和注解等功能。

第十章 Testing an Application,测试应用程序,涉及 Spring 测试。这包括单元测试、集成测试和 UI 测试,以及它们的应用。我们将了解项目的测试结构,以及 JUnit 和 Espresso 等测试工具。还将讨论 JUnit 和 JPA 的测试用例。我们将学习如何编写 Android 应用程序的 UI 测试用例。我们还将学习如何通过 Android Studio 执行这些测试。我们还将学习如何使用 Kotlin 中的 Espresso 测试 UI,以及它在 Android 应用程序中的相关应用。我们还将探讨应用程序中的并发和响应式编程。

为了充分利用本书

对 Spring 和 Kotlin 的基本了解将有所帮助,但不是必需的。运行本书的代码示例需要 MySQL Workbench 数据库、Eclipse 或 IntelliJ IDEA 用于 Spring、Android Studio 用于 Android,以及 Postman 或 Insomnia REST 客户端。

下载示例代码文件

您可以从www.packt.com的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packt.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packt.com登录或注册。

  2. 选择支持选项卡。

  3. 点击代码下载与勘误表。

  4. 在搜索框中输入书名,并遵循屏幕上的说明。

一旦文件下载完成,请确保您使用最新版本的以下软件解压缩或提取文件夹:

  • WinRAR/7-Zip(适用于 Windows)

  • Zipeg/iZip/UnRarX(适用于 Mac)

  • 7-Zip/PeaZip(适用于 Linux)

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Learn-Spring-for-Android-Application-Development/。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他丰富的图书和视频的代码包,可在github.com/PacktPublishing/找到。查看它们!

下载彩色图片

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

使用约定

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

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“switch { ... }控制流元素被when { ... }替换。”

代码块设置如下:

fun test() {
    Bar.NAME
    Bar.printName()
}

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

<!-- A bean example with singleton scope -->
<bean id = "..." class = "..." scope = "singleton"/>
<!-- You can remove the scope for the singleton -->
<bean id = "..." class = "..."/>

任何命令行输入或输出都应如下所示:

$ mkdir css
$ cd css

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“从管理面板中选择系统信息。”

警告或重要提示如下所示。

小贴士和技巧如下所示。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并给我们发送邮件至customercare@packtpub.com

勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将非常感激您能向我们报告。请访问www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上发现我们作品的任何非法副本,我们将非常感激您能提供位置地址或网站名称。请通过copyright@packt.com与我们联系,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

评论

请留下评论。一旦您阅读并使用了这本书,为何不在购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 团队可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

想了解更多关于 Packt 的信息,请访问packt.com

第一章:关于环境

本书标题提到了两个最大的平台名称——Spring,可能是 Java 最好的框架,以及 Android,它是任何操作系统中拥有最多用户的系统。本书将帮助您自学并开发一个轻量级、安全、强大且响应迅速的产品级应用程序。

在开始学习 Spring 和 Android 之前,我们将使用 Kotlin 演示示例和代码,因为这种编程语言对开发者来说非常新颖。如今,Kotlin 如此受欢迎,以至于谷歌已将其宣布为 Android 的官方语言。此外,Spring 语言也支持 Kotlin。在本书中,我们将探讨如何使用 Kotlin 语言构建一个健壮、安全且强大的基于 Spring 的服务器,并在 Android 应用程序中作为客户端使用该服务器的内容和功能。

在本章中,您将学习如何设置环境以创建 Spring 和 Android 项目,包括所需的工具和应用程序。这包括通过伴随图像进行可视化步骤。当时知道 Java 的开发者将有一些灵活性,因为它是 Spring 和 Kotlin 之间的通用平台。我们将使用在 JVM 上运行的 Kotlin 演示代码和模型。Kotlin 是由 JetBrains 设计的。如果您对 Kotlin 和 Spring 是新手,熟悉 Java 将使您能够轻松地用 Kotlin 编写代码。

本章将涵盖以下主题:

  • 设置环境

  • Spring

  • Java

  • Kotlin

  • Apache Tomcat

  • 集成开发环境

  • Android

技术要求

要运行这些框架,我们需要一些工具和特定的操作系统。以下是这些工具的列表:

  • 操作系统:推荐使用 Linux 和 macOS 进行开发,因为我们可以找到这些操作系统所需的所有软件包,并且它们比 Windows 轻量。

  • IDE:我推荐的 IDE 是 IntelliJ IDEA(终极版本)。这是 Java 最好的 IDE,但您必须购买才能使用。您也可以使用 Eclipse 和 Netbeans;其中之一对于开发 Spring 应用程序就足够了。我们将展示所有项目在 IntelliJ 中,但也会学习在 IntelliJ IDEA 和 Eclipse 中设置 Spring 环境的步骤。

您可以在 GitHub 上找到本章的所有示例:github.com/PacktPublishing/Learn-Spring-for-Android-Application-Development/

设置环境

环境设置是开发应用程序前的关键部分之一。对于目前正在使用 Spring 工作的开发者,请自由跳过这部分。本节是为新开发者准备的,他们需要建立基础和工具以开始开发。

以下是伴随段落中设置环境的步骤。

Spring

Spring 是最强大的 Java 应用程序框架;它目前在企业界中最为流行。它有助于创建易于测试和重用的代码,从而创建高性能的应用程序。这是一个开源项目,由 Rod Johnson 编写,于 2003 年 6 月首次以 Apache 2.0 许可证发布。

要创建和运行 Spring 应用程序,您需要一些工具和语言支持。您还需要一个服务器来在您的操作系统上测试和运行您的项目。我们将向您展示如何设置 Spring 的环境。

当前版本需要以下软件和工具:

  • Java (版本 1.8)

  • Kotlin (版本 1.3)

  • Apache Tomcat (版本 9.0.11)

  • IntelliJ Ultimate (版本 2018.2.2) 或 Eclipse Photon

  • Spring 框架库 (版本 5.0.8.RELEASE)

Java

Java 有两个版本:

  • 标准版 (J2SE)

  • 企业版 (J2EE)

在这里,我们将选择标准版。Java 对所有操作系统都是免费下载和使用的。

您可以从 www.oracle.com/technetwork/java/javase/downloads/index.html 下载 Java 10.0.2。

下载适用于您的操作系统。

安装后,请检查 Java 是否已安装。检查方法是在终端中输入 java --version。如果 Java 安装成功,您将看到以下 Java 版本:

检查 Java 版本

或者,你可能会看到一个错误。如果发生这种情况,请尝试重新安装以解决问题。

Kotlin

由 JetBrains 开发,Kotlin 是一个开源的静态类型编程语言。它运行在 Java 虚拟机 (JVM) 上,可以编译成 JavaScript 源代码或使用 LLVM 编译器基础设施。Kotlin 容易学习,尤其是对于 Java 开发者来说。

要使用 Kotlin,您不需要像 Java 那样单独下载或设置它。它包含在 IDE 中。Kotlin 是 Android Studio、IntelliJ Ultimate 或 IntelliJ Community 的内置功能。要在 Eclipse 中使用 Kotlin,您需要遵循以下步骤:

  1. 从 Eclipse 工具栏中选择帮助 -> Eclipse Marketplace

  2. 在搜索框中输入 Kotlin,在那里您将找到 Kotlin 插件。

  3. 安装它后,您就可以使用 Kotlin 编写代码:

Eclipse Marketplace

我们强烈推荐使用 IntelliJ IDE 来实现 Kotlin 的最新版本。Eclipse 插件没有 Kotlin 的最新版本。

Apache Tomcat

我们需要一个稳定、免费和开源的 Web 服务器,我们可以用它来创建和运行基于 Spring 框架的企业项目。我们将使用 Apache Tomcat,这对于所有 Java 开发者来说都很容易理解。您也可以使用 Jetty 或 Undertow 在 Spring 中进行开发。

Tomcat 是一个开源的 Web 服务器。这允许使用 Java Servlets 和 JavaServer Pages (JSP) 来构建 Java 服务器。Tomcat 的核心部分是 Catalina。

Apache Tomcat 是一个 Web 服务器,而不是应用程序服务器。

您可以从 tomcat.apache.org/download-90.cgi 下载 Tomcat 9.0.11。

如果您使用的是 Tomcat 版本 9,则必须使用 Java 版本 8 或更高版本。根据 Apache Tomcat 源代码,此版本基于 Tomcat 8.0.x 和 8.5.x,并实现了 Servlet 4.0、JSP 2.3、EL 3.0、WebSocket 1.1 和 JASPIC 1.1 规范(Java EE 8 平台所需的版本)。

让我们看看如何配置和验证 Tomcat 服务器。

配置 Tomcat

您可以通过两种方式配置 Tomcat 服务器——要么使用终端,要么从 IDE 中进行。为了设置服务器,您必须从 tomcat.apache.org/download-90.cgi 下载 Tomcat 服务器的内容。

按照以下步骤配置 Tomcat:

  1. 从链接下载核心模块的二进制发行版。

  2. 解压文件。这将创建一个名为 apache-tomcat-9.0.11 的文件夹(版本号可能不同)。

  3. 为了方便访问,请将文件夹 Tomcat 重命名并移动到 /usr/local(适用于 Linux)或 /Library(适用于 macOS):

图片

项目文件

  • 对于 Linux,请按照以下步骤操作:
// If you have an older version of Tomcat, then remove it before using the newer one
sudo rm -rf /usr/local/Tomcat // To remove exist TomCat

sudo mv ~/Download/Tomcat /usr/local // To move TomCat from the download directory to your desire direction
  • 对于 macOS,请按照以下步骤操作:
// If you have an older version of Tomcat, then remove it before using the newer one
sudo rm -rf /Library/Tomcat // To remove exist TomCat
sudo mv Downloads/Tomcat /Library/     // To move TomCat from the download directory to your desire direction

要检查当前目录,请输入以下命令:

  • 对于 Linux 系统: cd /usr/local/Tomcat/

  • 对于 macOS 系统: cd /Library/Tomcat/

  1. 输入 ls 以查看该目录的列表:

图片

在终端检查 tomcat 文件

  1. 更改 /usr/local/Tomcat/Library/Tomcat 文件夹结构的所有权:
  • 对于 Linux 系统: sudo chown -R <your_username> /usr/local/Tomcat/

  • 对于 macOS 系统: sudo chown -R <your_username> /Library/Tomcat/

  1. 使所有脚本可执行:
  • 对于 Linux 系统: sudo chmod +x /usr/local/Tomcat/bin/*.sh

  • 对于 macOS 系统: sudo chmod +x /Library/Tomcat/bin/*.sh

  1. 要检查 Tomcat 的内容,请使用以下命令:
  • 对于 Linux 系统: ls -al /usr/local/Tomcat/bin/*.sh

  • 对于 macOS 系统: ls -al /Library/Tomcat/bin/*.sh

  1. 您可以看到每个文件都列出了 -rwxr-xr-x@,其中 -x 表示可执行。可执行权限向我们展示了访问文件的授权状态:

图片

在终端检查 tomcat 可执行文件

  1. 要启动和停止,请输入以下命令:
  • 对于 macOS 系统:
/Library/Tomcat/bin/startup.sh
/Library/Tomcat/bin/shutdown.sh
  • 对于 Linux:
/usr/local/Tomcat/bin/startup.sh
/usr/local/Tomcat/bin/shutdown.sh
  1. 要开启和关闭 Tomcat 服务器,请使用以下命令:

图片

验证 Tomcat

  1. 启动服务器后,打开浏览器并输入 http://localhost:8080,这将显示默认页面:

图片

默认 tomcat 本地托管

这就是我们如何在终端配置 Tomcat 的方法。

集成开发环境

当涉及到编写 Java 程序时,您可以使用任何文本编辑器。然而,我们鼓励您使用 集成开发环境IDE),因为它们提供了许多功能。IntelliJ IDEA 是付费 IDE,但您可以使用免费提供的 Eclipse 或 NetBeans。

我们可以使用 IDE 执行以下操作:

  • 管理 Tomcat

  • 在不需要记住方法和方法签名完整名称的地方开发应用程序和 Web 应用程序。

  • 突出显示编译错误

在本书中,我们将使用 Eclipse 和 IntelliJ IDEA。

您可以从 www.jetbrains.com/idea/download/ 下载 Ultimate 版本,该版本提供 30 天免费试用。

要下载 Eclipse,请访问 www.eclipse.org/downloads/packages/

对于 Spring,您应该下载 Java EE 开发者版本的 Eclipse IDE。

对于两者,一旦启动 IDE,它将要求选择工作区。您可以选择一个文件夹并给出该文件夹的路径。

IntelliJ IDEA

IntelliJ IDEA 是一种用于开发计算机软件的 Java 协同开发环境。它由 JetBrains 开发,可以作为 Apache 2 许可的社区版和商业版提供。两者均可用于商业开发。

IntelliJ IDEA ultimate 和 IntelliJ IDEA community 内置了 Kotlin 的最新版本。

Eclipse

Eclipse 是一种用于计算机编程的集成开发环境,是最常用的 Java IDE。它包含一个基本工作空间和一个可扩展的模块框架,用于调整环境。Eclipse 主要用 Java 编写,其主要用途是开发 Java 应用程序,但也可以通过模块使用其他编程语言开发应用程序,包括 Ada、ABAP、C、C++、C#、Clojure、COBOL、D、Erlang、Fortran、Groovy、Haskell、JavaScript、Julia、Lasso、Lua、NATURAL、Perl、PHP、Prolog、Python、R、Ruby(包括 Ruby on Rails 框架)、Rust、Scala 和 Scheme。

要在 Eclipse 中使用 Kotlin,您需要安装 Kotlin 插件。

Eclipse 没有最新的 Kotlin 版本。

创建项目后,您需要手动集成 Tomcat 服务器。但是,如果您使用 Spring Boot,则无需执行任何操作,因为其中已包含 Tomcat 服务器。

按照以下步骤创建 Web 项目并将 Tomcat 服务器集成到您的项目中:

  1. 访问“新建”>“新建动态 Web 项目”。

  2. 提供项目名称。

  3. 要集成 Tomcat,请点击“新建运行时”:

图片

新建项目创建

  1. 下载版本 9+,选择 Apache Tomcat v9.0,然后点击完成:

图片

tomcat 版本选择

  1. 选择最新的动态 Web 模块版本。

  2. 点击完成。

创建项目后,您将找到以下文件:

图片

项目文件

  1. 前往位于左下角的窗口中的“服务器”选项卡:

项目 IDE 界面

  1. 选择 localhost 上的 Tomcat v9.0 服务器。

  2. 点击开始按钮。

  3. 服务器启动后,通过在浏览器中访问 http://localhost:8080 来验证它。

  4. 如果一切正常,您可以从这里启动和停止 Tomcat 服务器。

Android

Android 是由 Google 开发的一种移动操作系统,基于修改后的 Linux 内核和其他开源软件,主要用于触摸屏移动设备,例如手机和平板电脑。此外,Google 还开发了 Android TV 用于电视,Android Auto 用于车辆,以及 Wear OS 用于手表,每个都有特定的用户界面。Android 的变体也用于物联网、高级相机、个人电脑和各种硬件。它最初由 Android Inc. 开发,Google 于 2005 年收购了该公司,Android 于 2007 年发布。第一台商业 Android 设备于 2008 年 9 月推出。自那时起,当前版本已经经历了多次重大发布,当前版本为 9 Pie,于 2018 年 8 月发布。Android 的核心源代码被称为 Android 开源项目 (AOSP),并采用 Apache 许可证授权。

在本书中,我们将了解如何在服务器上的 Spring 平台上创建 REST API、安全和数据库。我们还将学习如何创建 Android 应用程序并从服务器检索数据,以及作为客户端的使用。

Android Studio 是众多 IDE 中用于创建 Android 应用的主要 IDE。这是 Android 的官方 IDE。它基于 JetBrains 的 IntelliJ IDEA,专门为 Android 应用开发构建。

要下载 Android Studio,请访问 developer.android.com/studio/。在这里,您可以找到最新版本的 Android Studio 下载。最好的部分是,它包括 JRE、最新 SDK 以及其他重要的插件,用于开发。

下载 Android Studio 应用程序后安装。这个工具非常易于使用。

不要忘记更新并下载 SDK 平台的最新版本。要更新或安装新的 SDK 平台,请转到 SDK 管理器。在 SDK 平台中,您可以查看所有 Android 版本平台的列表。

如果您没有遇到任何麻烦就阅读并安装了环境,您就可以开始学习本书中的信息了。我们已经在 GitHub 上提交了代码,并在 技术要求 部分分享了链接,因此您可以使用该示例代码。

摘要

本章主要面向那些刚开始接触这个平台的新开发者。我们已经展示了使用一些特定工具和应用程序的设置过程,你也可以使用不同的工具和应用程序来开发你的项目。我们探讨了如何设置开发 Spring 和 Android 的环境。你现在已经熟悉了所有必需的工具和软件。现在你能配置你操作系统中的 Tomcat 服务器,并熟悉如何启动和停止服务器。你可以决定你需要哪个 IDE 来进行开发。我们还学习了无需任何麻烦即可安装 Android Studio 的过程。最后,没有使用工具或软件最新版本的特定标准。

在下一章中,我们将探讨 Kotlin,这是一种静态类型编程语言,也是 Android 的官方语言。

问题

  1. Spring 框架是基于 Java SE 还是 Java EE 构建的?

  2. 开发 Spring 时,Eclipse 和 IntelliJ IDEA 的替代 IDE 是什么?

  3. Tomcat 是一个 Web 服务器还是应用服务器?

  4. 运行 Spring 时,Tomcat 服务器的替代方案有哪些?

  5. Android Studio 是开发 Android 的 IDE 吗?

进一步阅读

第二章:Kotlin 概述

Kotlin 是官方的 Android 编程语言,并且是静态类型的。它与 Java 完全兼容,这意味着任何 Kotlin 用户都可以使用 Java 框架,并且可以无限制地混合 Kotlin 和 Java 的命令。在本章中,我们将介绍 Kotlin 的基础知识,并探讨如何设置环境。我们还将探讨其流程结构,例如 if { ... } else { ... } 表达式和循环。此外,我们还将探讨 Kotlin 的面向对象编程,包括类、接口和对象。函数也将被介绍,包括参数、构造函数和语法。

本章将涵盖以下主题:

  • 设置环境

  • 构建工具

  • 基本语法

  • 面向对象编程

  • 函数

  • 控制流程

  • 范围

  • 字符串模板

  • 空安全、反射和注解

技术要求

要运行本章中的代码,您只需安装 Android Studio 和 Git 即可。本章不需要任何额外的安装。

您可以在 GitHub 上找到本章的示例,链接如下:github.com/PacktPublishing/Learn-Spring-for-Android-Application-Development/tree/master/app/src/main/java/com/packt/learn_spring_for_android_application_development/chapter2

Kotlin 简介

Android Studio 的 3.0 版本由 Google 发布,并推广 Kotlin 作为 Android 开发的第一类语言。Kotlin 由 JetBrains 开发,与 Intellij IDEA 平台相同,这是 Android Studio 的基础。这种语言于 2016 年 2 月发布,在发布前已经开发了五年。将项目的代码库从 Java 转换为 Kotlin 很容易,熟悉 Java 的开发者可以在几周内学会 Kotlin。Kotlin 在发布前就已经很受欢迎,因为这种语言功能丰富,并且设计用于与 Java 兼容。以下图表显示了 Kotlin 和 Java 代码是如何编译成相同的字节码的:

图片

如您所见,我们的应用程序的一部分可以用 Java 编写,另一部分可以用 Kotlin 编写。kotlinc 编译器将 Kotlin 源代码编译成与 javac 编译器相同的字节码。

设置环境

要开始 Android 开发,您需要从 www.oracle.com/technetwork/java/javase/downloads/index.html 下载并安装 Java 开发工具包JDK)。您还需要从 developer.android.com/studio/ 下载并安装 Android Studio 集成开发环境IDE)。

要创建一个新的项目,启动 Android Studio 并按下创建一个新的 Android Studio 项目。然后,你应该输入项目名称和你的唯一应用程序 ID,如下面的截图所示:

图片

在前面的截图中,应用程序名称字段根据本书的名称填写,公司域名字段为packt.com。Android Studio 将这两个值连接起来,创建一个等于应用程序 ID 标识符的包名标识符。在我们的情况下,应用程序 ID 如下所示:

com.packt.learn_spring_for_android_application_development

构建工具

Android Studio 是 Android 开发的官方 IDE,它基于 IntelliJ IDEA 平台,并使用 Gradle 构建工具系统。一个典型的项目结构如下所示:

图片

build.gradle文件包含项目配置并管理库依赖项。要添加对 Spring for Android 扩展的依赖项,我们应该添加以下行:

repositories {
    maven {
        url 'https://repo.spring.io/libs-milestone'
    }
}

dependencies {
    //.......
    implementation 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
}

基本语法

语法是编程语言的一个重要部分,定义了一组必须应用于符号组合的规则。否则,程序无法编译,将被视为不正确。

本节将描述 Kotlin 的基本语法,包括以下主题:

  • 定义包

  • 定义变量

  • 定义函数

  • 定义类

定义包

打包是一种机制,允许我们将类、接口和子包分组。在我们的情况下,文件中包的声明可能如下所示:

package com.packt.learn_spring_for_android_application_development

文件中的所有公民都属于这个包,并且必须位于适当的文件夹中。

定义变量

在 Kotlin 中,我们可以使用val关键字定义只读变量,并可以使用var关键字定义可变变量。在 Kotlin 中,变量可以被定义为第一类公民,这意味着我们不需要创建一个包含变量的类或函数。相反,我们可以直接在文件中声明它们。

以下示例展示了如何定义只读和可变变量:

val readOnly = 3
var mutable = 3

定义函数

要定义一个函数,我们必须使用fun关键字;这也可以被声明为第一类公民。这意味着函数只能在一个文件中定义。我们将在函数部分更详细地介绍函数,但就目前而言,让我们看看一个简单的例子,该例子会改变mutable变量的值:

fun changeMutable() {
    mutable = 4
}

在前面的代码片段中,我们可以看到changeMutable函数可以在与mutable变量相同的文件中声明为第一类公民,或者在任何其他位置。

定义类

要定义一个类,我们必须使用class关键字。在 Kotlin 中,所有类默认都是最终的,如果我们想扩展一个类,我们应该使用open关键字声明它。一个包含readOnlymutable变量以及changeMutable方法的类可能看起来像这样:

class Foo {
    val readOnly = 3
    var mutable = 3

    fun changeMutable() {
        mutable = 4
    }
}

值得注意的是,作为类成员的函数被称为方法。通过这种方式,我们可以明确指定一个函数属于一个类。

面向对象编程

面向对象编程是一种基于可以表示数据的对象的编程语言模型。Kotlin 以与 Java 相同的方式支持面向对象编程,但更为严格。这是因为 Kotlin 没有原始类型和静态成员。相反,它提供了一个companion object

class Bar {
    companion object {
        const val NAME = "Igor"

        fun printName() = println(NAME)
    }
}

companion object 是在类初始化期间创建一次的对象。在 Kotlin 中,我们可以像在 Java 中的 static 一样引用 companion object 的成员:

fun test() {
    Bar.NAME
    Bar.printName()
}

然而,在底层,嵌套的 Companion 类被创建,我们实际上使用这个类的实例,如下所示:

Bar.Companion.printName();

此外,Kotlin 支持以下概念,这使得类型系统更加强大:

  • 可空类型

  • 只读和可变集合

  • 集合没有原始类型

最后一点意味着我们无法编译代码,如下面的截图所示:

这条消息意味着我们必须提供一个泛型来指定此集合的特定类型。

从面向对象编程的角度来看,Kotlin 支持与 Java 相同的功能。这包括封装、继承、多态、组合和委托。它甚至提供了一个语言级别的构造,有助于实现这些概念。

函数

要在 Kotlin 中定义一个函数,你必须使用 fun 关键字,如下所示:

fun firstClass() {
    println("First class function")
}

前面的代码片段演示了我们可以将函数声明为第一类公民。我们还可以将函数定义为类成员,如下所示:

class A {
    fun classMember() {
        println("Class member")
    }
}

一个 local 函数是在另一个函数中声明的函数,如下所示:

fun outer() {
    fun local() {
        println("Local")
    }

    local()
}

在前面的代码片段中,local 函数是在 outer 函数内部声明的。local 函数仅在它们被声明的函数的作用域内可用。如果我们想避免在函数内部重复代码,这种方法可能很有用。

本节将涵盖以下主题:

  • 函数式编程

  • 高阶函数

  • Lambda

函数式编程

Kotlin 特别支持一种函数式风格,允许我们以与变量相同的方式操作函数。这种方法为 Kotlin 带来了许多功能,以及更简洁地描述程序流程的新方法。

本小节将涵盖以下主题:

  • 声明式和命令式风格

  • 扩展函数

  • Kotlin 中的集合

声明式和命令式风格

我们过去在编写面向对象编程时使用命令式编程风格,但对于函数式编程来说,一种更自然的风格是声明式。声明式风格假设我们的代码描述了要做什么,而不是如何做,这与命令式编程的常规做法不同。

以下示例演示了函数式编程在特定情况下如何有用。让我们想象我们有一个数字列表,我们想要找到大于4的数字。在命令式风格中,这可能会如下所示:

fun imperative() {
val numbers = listOf(1, 4, 6, 2, 9)
for (i in 0 until numbers.lastIndex) {
if (numbers[i] > 4) {
println(numbers)
        }
    }
}

如您所见,我们必须使用大量的控制流语句来实现这个简单的逻辑。在声明式风格中,它可能看起来如下:

fun declarative() {
    println(listOf(1, 4, 6, 2, 9).find { it > 4 })
}

前面的代码片段展示了函数式编程的强大功能。这段代码看起来简洁易读。Kotlin 标准库包含许多扩展函数,这些函数扩展了列表类型的功能。

扩展函数

Kotlin 的扩展函数特性与函数式编程无关,但最好在继续前进之前解释这个概念。这个特性允许我们通过不使用继承或任何软件设计模式(如装饰者模式),向一个类或类型添加新的功能。

在面向对象编程中,装饰器是一种设计模式,允许我们动态地向一个对象添加行为,而不影响同一类中的其他对象。

在以下代码片段中,extension函数被添加到A类的功能中:

fun A.extension() {
    println("Extension")
}

如您所见,使用这个特性很容易。我们只需要指定一个类名,在点号后面声明一个函数名。现在,我们可以像通常一样调用扩展函数:

fun testExtension() {
    A().extension()
}

Kotlin 中的集合

我们之前看到的find函数包含在 Kotlin 标准库的Collections.kt文件中。这个文件包含许多扩展函数,这些函数将函数式方法引入 Kotlin,并扩展了 Java 集合的功能,以便简化与之相关的操作。

集合是一系列用于存储和操作一组对象的类和接口的层次结构。

Collections.kt文件中最常见的函数如下:

  • filter:这个函数返回一个新列表,其中只包含匹配了传入谓词的元素

  • find:这个函数返回一个匹配了传入谓词的元素

  • forEach:这个函数对每个元素执行一个批准的操作

  • map:这个函数返回一个新列表,其中每个元素都根据传入的函数进行了转换

所有这些都被称为高阶函数。

高阶函数

如果一个函数可以接收或返回另一个函数,那么它被称为高阶函数。以下图例展示了高阶函数的不同情况。

第一个图例演示了f函数接收 lambda 并返回一个简单对象的情况:

第二个图例演示了f函数接收一个对象并返回一个函数的情况:

最后,第三个图例演示了一个f函数接收并返回 lambda 的情况:

让我们看看firstOrNull函数的实现,如下所示:

public inline fun <T> Iterable<T>.firstOrNull(predicate: (T) -> Boolean): T? {
    for (element in this) if (predicate(element)) return element
    return null
}

firstOrNull函数是一个扩展函数,它接受一个 Lambda 作为参数,并以通常的方式调用它——predicate(element)。它返回集合中第一个匹配predicate的元素;如果没有其他元素满足条件,则为null

Lambdas

Lambda 是一个未声明的函数。这在我们需要执行一个动作,但又不需要为它定义一个函数时很有用,因为我们只会使用它一次,或者只在一个作用域中使用。Lambda 是一个表达式,意味着它返回一个值。Kotlin 中的所有函数都是表达式,甚至一个函数的作用域也不包含return关键字;它返回一个在末尾评估的值。

以下 Lambda 表达式返回一个隐式的Unit类型的对象:

{x: Int -> println(x)}

Unit对象的声明如下:

public object Unit {
    override fun toString() = "kotlin.Unit"
}

Lambda 的引用可以保存到变量中:

val predicate: (Int) -> Unit = { println(it) }

我们可以使用这个变量来调用保存的 Lambda:

predicate(3)

控制流元素

在 Kotlin 中,控制流元素是表达式。这与 Java 不同,在 Java 中它们是语句。语句仅指定程序的流程,不返回任何值。本节将涵盖以下控制流元素:

  • if { ... } else { ... }表达式

  • when { ... }表达式

if { ... } else { ... }表达式

在 Kotlin 中,if控制流元素可以像在 Java 中使用一样使用。以下示例演示了if作为常规语句的使用:

fun ifStatement() {
    val a = 4
    if (a < 5) {
        println(a)
    }
}

如果你使用if { ... } else { ... }控制流元素作为表达式,你必须声明else块,如下所示:

fun ifExpression() {
    val a = 5
    val b = 4
    val max = if (a > b) a else b
}

前面的示例显示if { ... } else { ... }返回一个值。

when { ... }表达式

Kotlin 中的switch { ... }控制流元素被when { ... }所取代。Kotlin 中的when { ... }元素比 Java 中的switch { ... }元素更加灵活,因为它可以接受任何类型的值。一个分支只需要包含一个匹配条件。

以下示例演示了如何使用when { ... }作为语句:

fun whenStatement() {
    val x = 1
    when (x) {
        1 -> println("1")
        2 -> println("2")
        else -> {
            println("else")
        }
    }
}

前面的代码片段包含else分支,对于具有语句的案例,它是可选的。如果所有其他分支都没有匹配的条件,则调用else分支。如果您使用when { ... }作为表达式,并且编译器无法确定所有可能的案例都被覆盖,则else分支是必需的。以下表达式返回Unit

fun whenExpression(x: Int) = when (x) {
    1 -> println("1")
    2 -> println("2")
    else -> {
        println(x)
    }
}

如您所见,表达式提供了一种更简洁的编写代码的方式。为了确保您的分支覆盖了所有可能的案例,您可以使用枚举或 Sealed 类。

Enum 是一个特殊类型的类,用于定义一组常量。Sealed 类是一个具有受限子类层次结构的父类。所有子类都必须在 Sealed 类所在的同一文件中定义。

在 Kotlin 中,枚举的工作方式与 Java 中的方式类似。如果我们想限制类层次结构,可以使用密封类。这如下所示:

  1. 您应该使用sealed关键字声明一个类

  2. 您密封类的所有继承者必须在与其父类相同的文件中声明。

以下示例演示了如何实现这一点:

sealed class Method
class POST: Method()
class GET: Method()

使用when { ... }表达式,我们可以使用Method类型的类,如下所示:

fun handleRequest(method: Method): String = when(method) {
    is POST -> TODO("Handle POST")
    is GET -> TODO("Handle GET")
}

如您所见,使用这种方法,我们不必使用else分支。

循环

循环是一个特殊语句,允许我们重复执行代码。Kotlin 支持两种类型的循环,如下所示:

  • for

  • while

for 循环

for循环语句允许我们迭代任何包含iterate()方法的任何内容。反过来,这通过鸭子类型原则提供了一个符合迭代器接口的实例。

鸭子类型原则意味着如果一个接口包含的所有方法都被实现,则该接口被隐式实现。

Iterator接口如下所示:

public interface Iterator<E> {

    boolean hasNext();

    E next();
}

如果我们想将iterator()hasNext()next()方法作为类成员提供,我们必须使用operator关键字声明它们。以下示例演示了这种情况:

class Numbers(val numbers: Array<Int>) {

    private var currentIndex: Int = 0

    operator fun iterator(): Numbers = Numbers(numbers)

    operator fun hasNext(): Boolean = currentIndex < numbers.lastIndex

    operator fun next(): Int = numbers[currentIndex ++]
}

Numbers类可以如下使用:

fun testForLoop() {
    val numbers = Numbers(arrayOf(1, 2, 3))
    for (item in numbers) {
        //......
    }
}

使用扩展函数的实现如下:

class Numbers(val numbers: Array<Int>)

private var currentIndex = 0
operator fun Numbers.iterator(): Numbers {
    currentIndex = 0
    return this
}
operator fun Numbers.hasNext(): Boolean = currentIndex < numbers.lastIndex
operator fun Numbers.next(): Int = numbers[currentIndex ++]

如您所见,扩展函数使我们能够使现有的类可迭代。

while 循环

while() { ... }do { ... } while()语句的工作方式与 Java 中的方式相同。while语句接受一个条件,而do指定了一个在条件为true时应调用的代码块。以下示例演示了do { ... } while()在 Kotlin 中的样子:

fun testWhileLoop() {
    val array = arrayOf(1, 2, 3)
    do {
        var index = 0
        println(array[index++])
    } while (index < array.lastIndex)
}

如您所见,do { ... } while构造与在其他 C-like 语言中的工作方式相同。

范围

Kotlin 支持范围的概念,它表示可比较类型的序列。要创建范围,我们可以使用在类中实现的rangeTo方法,如下所示:

public operator fun rangeTo(other: Byte): LongRange = LongRange(this, other)

public operator fun rangeTo(other: Short): LongRange = LongRange(this, other)

public operator fun rangeTo(other: Int): LongRange = LongRange(this, other)

public operator fun rangeTo(other: Long): LongRange = LongRange(this, other)

因此,我们有两种创建范围的方法,如下所示:

  • 使用rangeTo方法。这可能看起来如下——1.rangeTo(100)

  • 使用..运算符。这可能看起来如下——1..100

范围在我们使用循环时非常有用:

for (i in 0..100) {
    // .....
}

0..100范围等于1 <= i && i <= 100语句。

如果您想排除最后一个值,可以使用until函数,如下所示:

0 until 100

我们还可以使用step函数,如下所示:

1..100 step 2

前面的代码片段表示如下范围:

[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, ... 99]

值得注意的是,范围支持许多until函数,例如filtermap

(0..100)
        .filter { it > 50 }
        .map { it * 2 }

字符串模板

Kotlin 支持一个更强大的功能——字符串模板。字符串可以包含可执行的代码表达式,并将它们的结果连接到字符串中。字符串模板的语法假设我们在表达式的开头使用 $ 符号。如果表达式包含一些评估,它必须被大括号包围。

字符串模板的最简单用法如下所示:

var number = 1
val string = "number is $number" 

包含表达式的更高级示例如下:

val name = "Igor"
val lengthOfName = "length is ${name.length}"

如您所见,字符串模板功能允许我们以比通常的连接或 StringBuilder 类更简洁的方式编写代码。

空安全、反射和注解

尽管我们已经涵盖了与 Kotlin 基本概述相关的大部分常见主题,但仍有一些更多的话题需要涉及。

本节将介绍以下主题:

  • 空安全

  • 反射

  • 注解

空安全

与 Java 相比,Kotlin 支持更严格的类型系统,并将所有类型分为两组,如下所示:

  • 可空

  • 不可空

应用程序崩溃的最常见原因之一是 NullPointerException。这发生在访问 null 引用的成员时。Kotlin 提供了一种机制,通过使用类型系统来帮助我们避免这种错误。

以下图表显示了 Kotlin 中的类层次结构看起来像什么:

图片

在 Kotlin 中,可空类型与不可空类型的名称相同,只是在末尾带有 ? 字符。

如果我们使用不可空变量,则不能将其赋值为 null,以下代码无法编译:

var name = "Igor"
name = null

要能够编译此代码,我们必须显式声明 name 变量为可空:

var name: String? = "Igor"
name = null

在完成此操作后,我们无法编译以下代码:

name.length

要访问可空类型的成员,我们必须使用 ?. 操作符,如下例所示:

name?.length

一个表达式可以多次包含 ?. 操作符,所需次数如下:

name?.length?.compareTo(4)

如果链中的某个成员为 null,则无法调用下一个成员。

为了提供一个替代的程序流程,如果遇到 null,我们可以使用 Elvis 操作符 (?:)。这可以按以下方式使用:

name?.length?.compareTo(4) ?: { println("name is null") }()

前面的代码片段演示了,如果我们想在一个表达式返回 null 时调用代码块,可以使用 Elvis 操作符。

反射

反射允许我们在运行时对代码进行元编程;这是通过一组语言和标准库功能实现的。Kotlin 标准库包含 kotlin.reflect 包,该包反过来包含表示元素引用的类,例如类、函数或属性。

要获取元素引用,我们应该使用 :: 操作符。以下示例演示了如何获取类引用:

val reference: KClass<String> = String::class

如您所见,类引用由 KClass 类表示。

函数引用也可以传递给高阶函数。以下示例显示了这可能看起来像什么:

fun isOdd(number: Int): Boolean = number % 2 == 0
val odds = listOf(1, 2, 3, 4, 5).filter(::isOdd)

属性的引用由 KProperty 类表示,并且可以通过以下方式获得:

val referenceToOddsPreperty = ::odds

KProperty 是一个表示类属性的类,它可以用来检索元数据,例如名称或类型。

注释

注解用于将元数据附加到代码。这是使用 annotation 关键字创建的:

public annotation class JvmStatic

在最常见的情况下,注解由注解处理工具使用,以生成或修改代码。让我们看看以下示例:

class Example1 {
    companion object {
        fun companionClassMember() {}
    }
}

Kotlin 字节码查看器显示了以下代码:

public final class Example1 {
   public static final Example1.Companion Companion = new Example1.Companion((DefaultConstructorMarker)null);

   public static final class Companion {
      public final void companionClassMember() {
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

如你所见,Example1 类包含嵌套的 Companion 类,该类包含 companionClassMember 方法。当使用 @JvmStatic 注解和反编译后的 Java 代码版本时,我们可以标记 companionClassMember 方法,其代码如下:

public final class Example1 {
   public static final Example1.Companion Companion = new Example1.Companion((DefaultConstructorMarker)null);

   @JvmStatic
   public static final void companionClassMember() {
      Companion.companionClassMember();
   }

   public static final class Companion {
      @JvmStatic
      public final void companionClassMember() {}

      private Companion() {}

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

前面的代码片段包含在 Example1 类中定义的额外静态 companionClassMember 函数,该函数调用 Companion 类的方法。使用 @JvmStatic 注解,我们告诉编译器生成一个额外的可以从 Java 端使用的方法。

摘要

在本章中,我们仔细研究了 Kotlin 的基本语法。我们还介绍并探讨了某些特性的示例,例如 lambda 表达式、字符串模板和范围。此外,你了解到控制流元素,如 if { ... } else { ... }when { ... },可以用作表达式,可以使我们的代码更加简洁和易读。

在下一章中,我们将概述 Spring 框架。

问题

  1. 什么是 Kotlin?

  2. Kotlin 如何支持面向对象编程?

  3. Kotlin 如何支持函数式编程?

  4. 我们如何在 Kotlin 中定义变量?

  5. 我们如何在 Kotlin 中定义函数?

进一步阅读

Kotlin 快速入门指南 (www.packtpub.com/application-development/kotlin-quick-start-guide),作者 Marko Devcic,由 Packt 出版。

第三章:Spring 框架概述

Spring 是一个强大、轻量级的应用程序框架,为各种框架提供支持,如 Hibernate、Struts 和 JSF。Spring 框架是构建最复杂、最安全和最健壮产品的顶级企业框架之一。这个框架在 Java 开发者中非常受欢迎,因为大多数在 Java 企业中工作的开发者都在使用 Spring。如今,Spring 支持 Kotlin 语言,因此它越来越受到其他语言用户的欢迎。在这本书中,我们将使用 Kotlin 开发 Spring 项目。

在本章中,我们将学习 Spring 框架的基础知识。我们将讨论 Spring 的基础知识,并查看一些使用 Spring MVC 和 SpringBoot 实现它们的示例。

本章涵盖以下主题:

  • Spring 简介

  • Spring 的优势

  • Spring 架构

  • 配置 Bean

  • Spring MVC

  • SpringBoot

技术要求

在 第一章,关于环境,我们展示了如何设置环境以及开发 Spring 所需的工具、软件和 IDE。首先,访问 start.spring.io/ 创建您的第一个项目。以下选项将可供选择:

  • Maven 项目或 Gradle 项目(我们选择了 Maven)

  • 语言:Java 或 Kotlin(我们选择了 Kotlin)

  • Spring Boot 版本:2.1.1 (SNAPSHOT)

一旦点击创建,您需要提供信息,例如 工件名称描述包名打包Java 版本

对于这个阶段,无需添加任何依赖。最后,生成项目并将其导入到您的 IDE 中。

本章的示例源代码可在 GitHub 上找到:github.com/PacktPublishing/Learn-Spring-for-Android-Application-Development/tree/master/Chapter03

Spring 简介

Spring 框架是一个开源框架。这是一个用 Java 编写的框架,由 Pivotal 软件开发。任何基于 Java 的企业应用程序都可以使用该框架的核心。Spring 框架使用 普通 Java 对象(POJO),这使得构建企业应用程序更加容易。

POJO 是不受任何限制(除了由 Java 语言规范强制实施的限制)的 Java 对象。POJO 用于提高应用程序的可读性和可重用性。

让我们在接下来的章节中学习 Spring 的优势和架构。

Spring 的优势

Spring 框架是一个组件丰富的框架,具有以下优势:

  • Spring 可用于独立应用程序、Web 应用程序和移动应用程序。

  • Spring 通过创建 依赖注入(DI)来提供免费耦合的解决方案。这提供了一个配置文件(或注解)来重新排列条件。

  • 它使用 面向切面编程 (AOP) 并使得隔离跨切面关注点,如日志记录、事务管理和安全性成为可能。

  • 它限制了样板代码。Spring 有大量的包和类,减少了编码并避免了样板代码。

  • 它支持不同的框架,例如 ORMHibernateLoggingJEE

  • Spring 提供了一种简单且安全的方法来处理登录框架、表单等。

  • 它处理 自动装配,这在构建复杂网络应用时可能是一个噩梦。

  • Spring Web 框架有一个 Web MVC 框架,它提供了比传统的 Web 框架更多的优势。

  • 它有能力移除单例和工厂类的创建。

  • Spring 框架包含了对监控业务对象并将它们的操作提供给引入层段的支持。

  • 它支持 XML注解 配置。

Spring 架构

Spring 框架是一个分层架构,由几个模块组成。所有模块都基于其核心容器的最高点。这些模块为开发者提供了在企业应用开发中可能需要的所有功能。无论如何,开发者可以选择他们需要的特性,并丢弃无用的模块。

模块化编程 是一种软件设计技术。它将程序的功能分离成独立的模块,以便每个模块包含一个特定的功能。

这里是 Spring 架构的图示:

Spring 框架大约有 20 个模块,它们被分为 核心容器数据访问/集成WebAOPInstrumentation测试

让我们了解 Spring 架构的组件。

核心容器

本节包括 核心BeansContext表达式语言 模块。

核心 模块是 Spring 架构的中心。这提供了诸如 控制反转 (IoC) 和 依赖注入 (DI) 等特性的实现。IoC 是 Spring 核心中的一个中心容器。DI 是 IoC 的另一个名称。这个容器负责创建对象形式并控制其完整生命周期。在这个过程中,系统创建依赖,容器在创建 Bean 时注入这些依赖。DI 的这个逆过程基本上被称为 IoC。

org.springframework.beansorg.springframework.context是 Spring 框架 IoC 的两大容器。IoC 有一个根接口,称为BeanFactory,由这些项目执行并持有各种 bean 定义,每个 bean 都有一个字符串名称识别。该接口提供了一个推动配置组件来处理项目。ApplicationContextBeanFactory的子接口,它包括更多应用层设置。例如,它包括WebApplicationContext,用于 Web 应用程序。ApplicationContext负责实例化、设计和收集 bean。

容器在配置元数据中指定了对象实例化、配置和对象组装的任务。配置元数据有三种配置方式:通过 XML、注解或代码。尽管我们使用 Kotlin 进行工作,但我们将以 Kotlin 语言编写代码和元数据。

下面是核心容器的简单流程图:

核心容器是使 Spring 项目准备就绪以查看输出的过程。在Java POJO 类(主要是业务对象和元数据,即配置元数据)的帮助下,Spring 容器将准备好的应用程序作为输出表示。

Bean模块代表一个 bean,这是一个由 IoC 容器组装、管理和实例化的对象。

上下文模块支持 EJB、JMS、基本远程通信等。ApplicationContext接口是上下文模块的并发点。

表达式语言模块通常用于在应用程序中执行逻辑,如数据查询、求和、除法和取模。为此,该模块提供了以下强大的表达式:

算术 +, -, *, /, %, ^
关系 <=, >=,<, >, ==, !=
逻辑 &&, &#124;&#124;, !
条件 ?, :
正则表达式 matches

数据访问/集成

数据访问/集成负责设置和获取公共或私有数据。它作为数据访问层和业务层之间的桥梁。以下是一些数据模块的名称:

  • JDBC: Java 数据库连接JDBC)帮助应用程序连接到数据库。

  • 对象关系映射: 这用作对象关系映射ORM)API 的集成层。

  • 对象/XML 映射: 这用作对象/XML 映射OXM)实现的集成层。

  • Java 消息服务: 这用于在 Spring 中为Java 消息服务JMS)提供支持。

  • 事务: 这用于为 POJO 类提供程序性和声明性事务管理。

Web

Web是 Spring MVC 框架的中心。我们还可以集成其他技术,如 JSF 和 Spring MVC。Web 提供了一些基本的集成功能,例如登录、注销、上传或下载文件。Web 层有四个模块:

  • Web: 这提供了基本的面向 Web 的集成功能。

  • Web-servlet: 此模块包含 Spring 对 Web 应用程序的 MVC 实现。

  • Web-struts: 此模块提供了一个增强和改进的框架,以使 Web 开发更加容易。

  • Web-portlet: 此模块是 Web MVC 框架的相同表示。

面向切面编程

面向切面编程AOP)是 Spring 框架的关键组件。这提供了一种新的思考程序结构的方法。AOP 可以在 Java 和 Kotlin 中实现。它可以在 bean 中进行配置。

AOP 将程序逻辑分割成某些部分,称为确认关注点。在任何企业应用程序中,都存在横切关注点,这些关注点应与基本业务逻辑分开。日志记录、事务处理、性能监控和安全被认为是应用程序中的横切关注点。

仪器化

仪器化是评估项目性能水平、分析错误和编写跟踪信息的能力。仪器化是 Spring 框架审计应用程序性能的关键亮点之一。Spring 通过 AOP 和日志记录支持仪器化。

测试

企业软件开发的一个基本部分是测试。JUnit 或 TestNG 可以用来测试 Spring 组件。这支持 Spring 元素的单元和集成测试。

配置 Bean

Bean 是由 Spring IoC 实例化和组装的对象。这些 Bean 是通过配置 Spring 的元数据创建的。以下是一组表示每个 Bean 定义的属性:

  • 名称

  • 作用域

  • 构造函数参数

让我们在以下章节中了解配置元数据的用途。

Spring 配置元数据

提供配置元数据的三个主要功能如下:

  • 基于 XML 的配置

  • 基于 Kotlin/Java 注解的配置

  • 基于 Kotlin/Java 代码的配置

基于 XML 的配置

基于 XML 的配置在 Spring 2.0 中引入,并在 Spring 2.5 和 3.0 中增强和扩展。转向基于 XML 的配置文件的主要原因是为了使 Spring XML 配置更容易。基于<bean/>经典方法很好,但也增加了一些可能在大项目中变得复杂的配置。

让我们看看一个基于 XML 的配置文件示例,其中包含各种 Bean 定义,包括作用域、初始化技术和销毁策略,然后我们将讨论这个问题。以下是bean.xml的代码片段:

<!-- A simple bean definition -->
<bean id = "..." class = "...">
<!-- collaborators and configuration-->
</bean>

<!-- A bean example with prototype scope -->
<bean id = "..." class = "..." scope = "prototype"> <!-- collaborators and configuration-->
</bean>

<!-- A bean definition with initialization function -->
<bean id = "..." class = "..." init-function = "...">
<!-- collaborators and configuration-->
</bean>

<!-- A bean definition with destruction function -->
<bean id = "..." class = "..." destroy-function = "...">
<!-- collaborators and configuration for this bean go here -->
</bean>

Bean 作用域

在定义一个 Bean 时,我们可以选择声明其作用域为扩展。例如,如果我们限制 Spring 每次都提供另一个 Bean 实例,我们可以将原型作用域初始化为 Bean 的一个属性。此外,如果我们需要 Spring 恢复一个类似的 Bean 实例,我们应该声明 Bean 的作用域属性为单例

Spring 框架支持以下五个作用域,其中三个在如果我们使用一个感知 Web 的ApplicationContext时是可用的。以下是一些常见的作用域:

  • 单例:每次都返回默认使用的相同实例

  • 原型:每次都返回不同的实例

  • 请求:定义一个在应用程序的单个 JSP 页面中可见的 HTTP 请求

  • 会话:定义一个在应用程序的所有 JSP 页面中可见的 HTTP 会话

单例作用域

默认作用域始终是单例。这是 Spring IoC 容器的一个 Bean 定义,它在每次对象初始化时返回一个单一的对象实例。以下是一个单例作用域的代码示例:

<!-- A bean example with singleton scope -->
<bean id = "..." class = "..." scope = "singleton"/>
<!-- You can remove the scope for the singleton -->
<bean id = "..." class = "..."/>

让我们看看一个单例作用域的示例。

在 IDE 中创建一个 Spring 项目。为此,在src文件夹下创建两个kt文件和一个 Bean XML 配置文件。

以下是一个CreateUserGreeting.kt的代码片段:

class UserGreeting {
    private var globalGreeting: String? = "Sasuke Uchiha"

    fun setGreeting(greeting: String) {
        globalGreeting = greeting
    }

    fun getGreeting() {
        println("Welcome, " + globalGreeting!! + "!!")
    }
}

BeansScopeApplication.kt的内容如下:

fun main(args: Array<String>) {
    val context = ClassPathXmlApplicationContext("Beans.xml")

// first object
    val objectA = context.getBean("userGreeting", UserGreeting::class.java)

// set a value for greeting
    objectA.setGreeting("Naruto Uzumaki")

    objectA.getGreeting()

    val objectB = context.getBean("userGreeting", UserGreeting::class.java)
    objectB.getGreeting()
}

以下是一个beans.xml配置文件:

<?xml version = "1.0" encoding = "UTF-8"?>
<beans xmlns = "http://www.springframework.org/schema/beans"
       xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation = "http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="userGreeting" class ="ktPackage.UserGreeting" scope="singleton"/>

</beans>

运行此项目后,您将看到以下输出:

Welcome, Naruto Uzumaki!!  <--- value of objectA 
Welcome, Naruto Uzumaki!!  <--- value of objectB 

原型作用域

原型作用域在每次对象初始化时都会创建一个 Bean 的新实例。这个作用域更适合有状态的 Bean。容器不管理这个原型作用域的完整生命周期。以下是一个原型作用域的代码示例:

<!-- A bean example with prototype scope -->
<bean id = "..." class = "..." scope = "prototype"/>

让我们看看一个原型作用域的示例。

重新使用之前的工程,并修改 Bean XML 配置文件,如下所示:

<?xml version = "1.0" encoding = "UTF-8"?>
<beans xmlns = "http://www.springframework.org/schema/beans"
       xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation = "http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="userGreeting" class ="ktPackage.UserGreeting" scope="prototype"/>

</beans>

一旦我们完成创建源文件和 Bean 配置文件,我们就可以运行应用程序。如果没有错误,我们将得到以下消息:

Welcome, Naruto Uzumaki!!  <--- value of objectA 
Welcome, Sasuke Uchiha!!  <--- value of objectB 

Bean 生命周期

有时,我们需要在 Bean 类中实例化资源。例如,这可以通过在初始化阶段(在处理任何客户请求之前)建立数据库连接或批准第三方服务来实现。Spring 框架提供了不同的方法,通过这些方法我们可以在 Spring Bean 的生命周期中提供介绍和销毁技术。

它们如下所示:

  • 通过实现 InitializingBeanDisposableBean 接口——这两个接口都宣布了一个单一策略,我们可以在这个策略中实例化/关闭资源。对于 post-instatement,我们可以执行 InitializingBean 接口并提供 afterPropertiesSet() 函数的实现。对于 pre-destroy,我们可以实现 DisposableBean 接口并提供 destroy() 函数的实现。这些函数是回调技术,类似于 servlet 监听器实现。这个功能易于使用,但并不推荐,因为它会使我们的 bean 实现与 Spring 框架紧密耦合。

  • 在 Spring 的 bean 配置文件中为 bean 提供初始化函数 init-function 和销毁函数 destroy-function 的质量值。这是规定的功能,因为没有立即依赖 Spring 框架。我们也可以创建自己的函数。

post-initpre-destroy 函数不应有任何冲突,但它们可以抛出异常。我们还需要从 Spring 应用程序设置中获取这些函数的 bean 事件。

让我们看看一个 bean 生命周期的例子。在这里,我们将看看如何初始化和销毁 bean 函数。重新使用之前的工程,并按如下修改 bean XML 配置文件:

<?xml version = "1.0" encoding = "UTF-8"?>
<beans xmlns = "http://www.springframework.org/schema/beans"
       xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation = "http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="userGreeting" class ="ktPackage.UserGreeting" init-function = "afterPropertiesSet" 
 destroy-function = "destroy"/>

</beans>

现在在 UserGreeting.kt 中添加两个函数:

class UserGreeting {
    private var globalGreeting: String? = "Sasuke Uchiha"

    fun setGreeting(greeting: String) {
        globalGreeting = greeting
    }

    fun getGreeting() {
        println("Welcome, " + globalGreeting!! + "!!")
    }

 fun afterPropertiesSet(){
 println("Bean is going to start.")
 }

 fun destroy(){
 println("Bean is going to destroy.")
 }
}

在类的 main 函数任务完成后调用 registerShutdownHook()

fun main(args: Array<String>) {
    val context = ClassPathXmlApplicationContext("Beans.xml")
    val objectA = context.getBean("userGreeting", UserGreeting::class.java)

    objectA.setGreeting("Naruto Uzumaki")
    objectA.getGreeting()
    context.registerShutdownHook()
}

输出将如下所示:

Bean is going to start.
Welcome, Naruto Uzumaki!!
Bean is going to destroy.

依赖注入

DI 是一个系统,其中对象的依赖由外部容器提供。Spring DI 帮助将类与其依赖项连接起来,并保持它们解耦,以便我们可以在运行时注入这些依赖项。

依赖项在 bean 配置中定义。使用 XML 注入对象的最常见两种方法是 构造函数注入setter 注入,我们现在将探讨它们:构造函数注入

构造函数注入将依赖项注入到类构造函数中。让我们看看构造函数注入的一个例子。重新使用之前的工程,并修改 beans.xml 的内容:

<?xml version = "1.0" encoding = "UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <!--Constructor-based Dependency Injection Example Start-->
    <bean id="userGreeting" class="ktPackage.UserGreeting">
 <constructor-arg ref="userSurname" />
    </bean>
 <bean id="userSurname" class="ktPackage.UserSurname"/>
    <!--Constructor-based Dependency Injection Example End-->
</beans>

constructor-arg 用于注入依赖项。constructor-arg 的引用是构造函数的对象。

创建一个 UserSurname.kt 类来查看构造函数注入的使用我们将从这个类中获取姓氏,如下所示:

class UserSurname {
 init {
 println("This is init of UserSurname")
 }

 fun getSurname(){
 println("This is the surname of user")
 }
}

初始化 UserSurname 并将 getUserSurname() 函数添加到 CreateUserGreeting.kt

// added a constractor of UserSurname
class UserGreeting(surname: UserSurname) {
    private var userSurname: UserSurname ?= surname
 init {
 println("It is a constructor for user's surname")
 }

    private var globalGreeting: String? = "Sasuke Uchiha"

    fun setGreeting(greeting: String) {
        globalGreeting = greeting
    }

    fun getGreeting() {
        println("Welcome, " + globalGreeting!! + "!!")
    }

    fun afterPropertiesSet(){
        println("Bean is going to start.")
    }

    fun destroy(){
        println("Bean is going to destroy.")
    }

    fun getUserSurname(){
 userSurname?.getSurname()
 }
}

现在,如果我们调用 BeansScopeApplication 中的 getUserSurname() 函数,我们将得到 UserSurname 类。

下面是 BeansScopeApplication.kt 的示例代码

fun main(args: Array<String>) {
    val context = ClassPathXmlApplicationContext("Beans.xml")
    val objectA = context.getBean("userGreeting", UserGreeting::class.java)
 objectA.getUserSurname()

//    objectA.setGreeting("Naruto Uzumaki")
//    objectA.getGreeting()
//    context.registerShutdownHook()
}

输出将如下所示:

This is init of UserSurname                <------ init from UserSurname.kt
It is a constructor for user's surname     <------ init from UserGreeting.kt
This is the surname of user                <------ getUserSurname() of UserGreeting.kt

setter 注入

在 Spring 中,setter 注入是一种 DI,其中框架使用setter函数将依赖于另一个对象的对象注入到客户中。容器首先调用无参构造函数,然后调用 setter。基于 setter 的注入将无论是否使用构造函数注入了一些依赖项都能正常工作。

让我们看看setter注入的一个示例。这里,重用之前的工程并修改beans.xml的内容:

<?xml version = "1.0" encoding = "UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<!--Setter Injection Example Start-->
    <bean id="userGreeting" class="ktPackage.UserGreeting">
        <property name="userSurnameClass" ref="userSurname"/>
    </bean>
    <bean id="userSurname" class="ktPackage.UserSurname"/>
    <!--Setter Injection Example End-->
</beans>

修改 bean 文件后,将UserSurname的 setter 和 getter 添加到CreateUserGreeting.kt文件中:

class UserGreeting {
    private var userSurname: UserSurname? = null

    fun setUserSurnameClass(surname: UserSurname) {
 userSurname = surname
 }

 fun getUserSurnameClass(): UserSurname? {
 return userSurname
    }

    private var globalGreeting: String? = "Sasuke Uchiha"

    fun setGreeting(greeting: String) {
        globalGreeting = greeting
    }

    fun getGreeting() {
        println("Welcome, " + globalGreeting!! + "!!")
    }

    fun afterPropertiesSet() {
        println("Bean is going to start.")
    }

    fun destroy() {
        println("Bean is going to destroy.")
    }

    fun getUserSurname() {
        userSurname?.getSurname()
    }
}

结果将如下所示:

This is init of UserSurname
Setting User Surname in UserGreeting
This is the surname of user

空字符串或null值的示例如下:

<bean id="app" class="App">
<property name="name" value=""/>
</bean>
<!-- If we need to pass an empty string or null as a value -->
<bean id="app" class="App">
<property name="name"><null/></property>
</bean>

自动装配 bean

我们一直使用<constructor-arg><property>来注入依赖项。相反,我们可以自动装配依赖项,这有助于减少需要组成的配置量。

自动装配有多种选择,这些选择管理 Spring 容器以最有效的方式注入条件。默认情况下,bean 没有自动装配。

这里列出了两种主要的自动装配类型:

  • byName:要自动装配一个 bean,Spring 容器通过类名选择 bean。下面是byName使用的一个示例:
<bean id="app" class="App" autowire="byName"/>
  • byType:要自动装配一个 bean,Spring 容器根据类类型选择 bean。下面是byType使用的一个示例:
<bean id="app" class="App" autowire="byType"/>

如果一个Service接口有多个实现类,你会遇到两种情况。

services(一组services执行Service接口)的情况下,bean 不允许我们执行byName的自动装配。如果没有byName的发生,它将注入所有执行对象。

mainService(一个对象实现了Service接口)的情况下,对于byType/构造函数,在所有执行类的<bean>标签中将自动装配申请属性设置为false,保持其中一个为true

下面是如何在beans.xml中处理Service接口多个实现类的一个示例:

<?xml version = "1.0" encoding = "UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <!--Beans Auto-Wiring Example Start-->
    <bean id="userGreeting" class="ktPackage.UserGreeting" autowire="byType"/>
    <bean id="userSurname" class="ktPackage.UserSurname" autowire-candidate="true"/>
    <bean id="xxxxx" class="ktPackage.XXXX" autowire-candidate="false"/> <!--demoClass-->
    <bean id="yyyyy" class="ktPackage.YYYY" autowire-candidate="false"/> <!--demoClass-->
    <!--SBeans Auto-Wiring Example End-->
</beans>

对于byName,可以在应用程序类中将mainService重命名为实际化类之一(即userSurname),或者在该类的 XML 配置中将 bean 的id重命名为mainService

<?xml version = "1.0" encoding = "UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <!--Beans Auto-Wiring Example Start-->
    <bean id="userGreeting" class="ktPackage.UserGreeting" autowire="byName"/>
    <bean id="mainService" class="ktPackage.UserSurname"/>
    <bean id="xxxxx" class="ktPackage.XXXX"/> <!--demoClass-->
    <bean id="yyyyy" class="ktPackage.YYYY"/> <!--demoClass-->
    <!--SBeans Auto-Wiring Example End-->
</beans>

这里列出了自动装配的一些限制:

  • 覆盖可能性:要指定依赖项,可以使用<constructor-arg><property>设置,这将覆盖自动装配。

  • 原始数据类型:原始数据类型、字符串和类不能被调用。

  • 混淆性:自动装配不如明确装配准确。

基于注解的配置

注解是 DI 的新技术它从 Spring 2.5 开始被使用。不需要任何 XML 文件来维护配置。要使用基于注解的配置,你需要创建一个组件类,在其中你可以实现 bean 配置。注解是在相关的类、函数或字段上独特的名称或标记。

假设你已经熟悉@Override,这是一个告诉编译器此注解是一个废弃函数的注解。

在前面的注解中,Spring 框架的行为在很大程度上是通过 XML 配置控制的。今天,通过我们设计 Spring 框架实践的方式,注解的使用给我们带来了许多优势。

下面是一段bean.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-3.0.xsd
   http://www.springframework.org/schema/context
   http://www.springframework.org/schema/context/spring-context.xsd">

     <context:annotation-config/>
   <!-- bean definitions will be from here -->
</beans>

如果我们在bean.xml中使用<context:annotation-config/>,我们就可以开始注解代码,将值连接到属性、函数或构造函数中。在接下来的几节中,我们将学习一些基本注解。

@Required注解

@Required注解应用于 bean 的属性 setter 函数。bean 属性必须在配置时在 XML 配置文件中填充。此注解本质上表明 setter 函数必须安排在配置时使用值进行依赖注入。

添加一个用户模型和Main类,并使用bean.xml配置文件。

bean.xml配置文件的内容如下:

<?xml version = "1.0" encoding = "UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
   http://www.springframework.org/schema/context
   http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>  <!--after this tag, we have to write the beans-->

    <bean id="users" class="requiredAnnotation.UsersForReq">
 <property name="name" value="Naruto Uzumaki"/>
        <property name="village" value="Konohagakure"/>
    </bean>
</beans>

UsersForReq.kt的内容如下:

class Users{
    private var village: String? = null
    private var name: String? = null

 @Required    fun setVillage(village: String?) {
        this.village = village
    }

    fun getVillage(): String? {
        return village
    }

 @Required    fun setName(name: String) {
        this.name = name
    }

    fun getName(): String? {
        return name
    }
}

AnnotationBasedReqApp.kt的内容如下:

fun main(args: Array<String>) {
    val context = ClassPathXmlApplicationContext("requiredAnnotation/beans_for_req.xml")
    val users = context.getBean("users") as UsersForReq

    println("Name: "+users.getName())
    println("Village: "+users.getVillage())
}

此项目的输出将如下所示:

Name: Naruto Uzumaki
Village: Konohagakure

@Autowired注解

@Autowired注解帮助我们连接构造函数、字段和 setter 函数。此注解注入对象依赖项。

下面是使用@Autowired注解在属性上的示例代码:

class User(val name: String,
            val id: String)

class Users{
    @Autowired
    val user:User ?= null
}

下面是使用@Autowired注解在属性上的示例代码:

class UsersForAutowired{
    private lateinit var userDetails: UserDetails

    @Autowired
    fun setUserDetails(userDetails: UserDetails){
        this.userDetails = userDetails
    }

    fun getUserDetails(){
        this.userDetails.getDetails()
    }
}

UserDetails.kt的内容如下:

class UserDetails{
    init {
        println("This class has all the details of the user")
    }

    fun getDetails(){
        println("Name: Naruto Uzumaki")
        println("Village: Konohagakure")
    }
}

项目的输出将如下所示:

This class has all the details of the user
Name: Naruto Uzumaki
Village: Konohagakure

我们可以利用@Autowired注解在属性上,以消除 setter 函数。当我们使用<property>传递自动装配属性的值时,Spring 将使用传递的值或引用分配这些属性。因此,使用属性上的@AutowiredUsersForAutowired.kt文件将如下所示:

class UsersForAutowired{
    init {
 println("UsersForAutowired constructor." )
 }

 @Autowired
    private lateinit var userDetails: UserDetails

    fun getUserDetails(){
        this.userDetails.getDetails()
    }
}

结果将如下所示:

UsersForAutowired constructor.
This class has all the details of the user
Name: Naruto Uzumaki
Village: Konohagakure

你也可以将@Autowired应用于构造函数。@Autowired构造函数注解表明在创建 bean 时应该自动装配构造函数。这应该是在 XML 文件中配置 bean 时,无论是否使用了<constructor-arg>组件的情况下。

下面是UsersForAutowired.kt修改后的内容:

class UsersForAutowired @Autowired constructor(private var userDetails: UserDetails) {
    init {
        println("UsersForAutowired constructor.")
    }

    fun getUserDetails() {
        this.userDetails.getDetails()
    }
}

结果将如下所示:

This class has all the details of the user
UsersForAutowired constructor.
Name: Naruto Uzumaki
Village: Konohagakure

@Qualifier注解

您可能创建了多个相同类型的 bean,但只需要通过属性连接其中一个。在这种情况下,您可以使用@Qualifier注解与@Autowired一起使用,通过确定哪个正确的 bean 将被连接来消除混乱。在本节中,我们将通过一个示例来展示@Qualifier注解的使用。

bean.xml配置文件的内容如下:

<?xml version = "1.0" encoding = "UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
   http://www.springframework.org/schema/context
   http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>  <!--after this tag, you have to write the beans-->

    <!-- Definition for Fighters bean without constructor-arg  -->
    <bean id="fighters" class="qualifierAnnotation.Fighters"/>

    <!--fighter 1-->
    <bean id="fighter1" class="qualifierAnnotation.UsersForQualifier">
        <property name="name" value="Naruto Uzumaki"/>
        <property name="village" value="Konohagakure"/>
    </bean>

 <!--fighter 2-->
    <bean id="fighter2" class="qualifierAnnotation.UsersForQualifier">
        <property name="name" value="Gaara"/>
        <property name="village" value="Sunagakure"/>
    </bean>
</beans>

下面是AnnotationBasedQualifierApp.kt的内容:

fun main(args: Array<String>) {
    val context = ClassPathXmlApplicationContext("qualifierAnnotation/beans_for_qualifier.xml")
    val fighters = context.getBean("fighters") as Fighters
    fighters.getName()
    fighters.getVillage()
}

现在,添加另一个类。以下是UsersForQualifier.kt的内容:

class UsersForQualifier{
    private var village: String? = null
    private var name: String? = null

    fun setVillage(village: String?) {
        this.village = village
    }

    fun getVillage(): String? {
        return village
    }

    fun setName(name: String) {
        this.name = name
    }

    fun getName(): String? {
        return name
    }
}

最后,添加Fighters.kt类。以下是该类的内容:

class Fighters {
    @Autowired
 @Qualifier("fighter1")
    lateinit var usersForQualifier: UsersForQualifier

    init {
        println("Fighters constructor.")
    }

    fun getName() {
        println("Name: " + usersForQualifier.getName())
    }

    fun getVillage() {
        println("Village: " + usersForQualifier.getVillage())
    }
}

如果您运行输出,它将是以下内容:

Fighters constructor.
Name: Naruto Uzumaki
Village: Konohagakure

修改限定符值如下:

 @Qualifier("fighter2")

它将创建以下输出:

Fighters constructor.
Name: Gaara
Village: Sunagakure

基于代码的配置

我们看到了如何通过使用 XML 配置文件来设计 Spring beans。如果您习惯于 XML 配置,您可以忽略这个主题。

基于代码的配置选项使您能够在不使用 XML 的情况下编写大部分 Spring 配置。

@Configuration@Bean注解

在一个类上使用@Configuration注解意味着这个类将被 Spring IoC 容器使用,并被视为 bean 定义的来源。

在一个函数上使用@Bean注解意味着该函数将返回一个对象,该对象将被注册为 Spring 应用程序上下文中的一个 bean。

下面是@Configuration@Bean的示例代码:

@Configuration
open class CodeBasedConfiguration{
 @Bean
  open fun mainApp(): MainApp{
      return MainApp()
  }
}

之前的代码将与以下 XML 配置等效:

<beans>
  <bean id = "mainApp" class = "MainApp"/>
</beans>

在这里,函数名通过@Bean注解进行注释,这创建了并返回了 bean 定义。您的配置类可以有多个@Bean

GreetingConfigurationConfBean.kt的内容如下:

@Configuration
open class GreetingConfigurationConfBean{
 @Bean
  open fun greeting(): GreetingConfBean{
      return GreetingConfBean()
  }
}

GreetingConfBean.kt的内容如下:

class GreetingConfBean{
    private var users: String? = null
    fun setUsers(users: String) {
        this.users = users
    }
    fun getUsers() {
        println("Welcome, $users!!")
    }
}

MainAppConfBean.kt的内容如下:

fun main(args: Array<String>) {
    val applicationContext = AnnotationConfigApplicationContext(GreetingConfigurationConfBean::class.java)

    val greeting = applicationContext.getBean(GreetingConfBean::class.java)
 greeting.setUsers("Naruto Uzumaki")
    greeting.getUsers()
}

结果将是以下内容:

Welcome, Naruto Uzumaki!!

依赖注入 bean

注释@Bean注解以注入依赖项。以下是GreetingConfigurationDIBean.kt的内容

@Configuration
open class GreetingConfigurationDIBean{
    @Bean
    open fun greeting(): GreetingDIBean {
        return GreetingDIBean(getUserDetails())
    }

 @Bean
    open fun getUserDetails(): GreetingDetailsDIBean {
        return GreetingDetailsDIBean()
    }
}

当两个@Beans相互依赖时,这种依赖关系就像一个 bean 方法调用另一个 bean 一样简单。

GreetingDIBean.kt的内容如下:

class GreetingDIBean (private val userDetails: GreetingDetailsDIBean){
    init {
        println("Inside DependenciesInjectBean.GreetingDIBean constructor.")
    }

    fun getGreeting() {
        userDetails.getGreetingDetails()
    }
}

GreetingDetailsDIBean.kt的内容如下:

class GreetingDetailsDIBean{
    init {
        println("This class has all the details of the user")
    }

    fun getGreetingDetails(){
        println("Welcome, Naruto Uzumaki!!")
    }
}

MainApp.kt的内容如下:

fun main(args: Array<String>) {
    val applicationContext = AnnotationConfigApplicationContext(GreetingConfigurationDIBean::class.java)

    val greeting = applicationContext.getBean(GreetingDIBean::class.java)
    greeting.getGreeting()
}

结果将是以下内容:

This class has all the details of the user
Inside Greeting constructor.
Welcome, Naruto Uzumaki!!

@Import注解

Spring 的@Import注解提供了类似于 Spring XML 中的<import/>元素的功能。通过使用@Import注解,您可以导入至少一个@Configuration类。它还可以导入包含至少一个@Bean函数的类。

Boo.kt的内容如下:

class Foo{
    init {
        println("This is class Foo")
    }
}
class Boo{
    init {
        println("This is class Boo")
    }
}

ConfigBoo.kt的内容如下:

@Configuration class ConfigFoo {
 @Bean    fun foo(): Foo{
        return Foo()
    }
}

@Configuration
@Import(ConfigFoo::class)
class ConfigBoo {
 @Bean    fun foo(): Boo {
        return Boo()
    }
}

在实例化上下文时,您不需要指定ConfigFoo.classConfigBoo.class,因此当您初始化AnnotationConfigApplicationContext时,以下代码是不必要的:

val applicationContext = AnnotationConfigApplicationContext(ConfigBoo::class.java, ConfigFoo::class.java)

由于ConfigFoo的 bean 定义已经通过使用带有ConfigBoo bean 的@Import注解加载,因此只需显式指定ConfigBoo

val applicationContext = AnnotationConfigApplicationContext(ConfigBoo::class.java)

这里是MainAppImport.ktmain函数修改后的完整代码:

fun main(args: Array<String>) {
    val applicationContext = AnnotationConfigApplicationContext(ConfigBoo::class.java)

    //both beans Boo and Foo will be available...
    val boo: Boo = applicationContext.getBean(Boo::class.java)
    val foo: Foo = applicationContext.getBean(Foo::class.java)
}

结果将如下所示:

This is class Boo
This is class Foo

生命周期回调

@Bean注解支持确定可选的引入和销毁回调函数。如果您注意到了XMLBasedSpringConfiguration项目中的beans.xml,您可以在其中找到init-methoddestroy-method属性。以下是如何初始化init-methoddestroy-method属性的示例:

<bean id="userGreeting" class="ktPackage.UserGreeting" init-method="afterPropertiesSet" destroy-method="destroy"/>

这里是MainAppLifeCall.kt的修改代码:

fun main(args: Array<String>) {
    val applicationContext = AnnotationConfigApplicationContext(ConfigFoo::class.java)

    val foo: Foo = applicationContext.getBean(Foo::class.java)
    applicationContext.registerShutdownHook()
}

Foo.kt的修改代码如下:

class Foo{
    fun init(){
        println("Foo is initializing...")
    }

    fun destroy(){
        println("Foo is destroying...")
    }
}

现在为Foo创建一个配置类。ConfigFoo.kt的修改代码如下:

@Configuration
open class ConfigFoo {
    @Bean(initMethod = "init", destroyMethod = "destroy")
    open fun foo(): Foo {
        return Foo()
    }
}

此项目的输出将如下所示:

Foo is initializing...
Foo is destroying...

创建一个作用域 bean

使用@Configuration创建一个@Scope bean 以创建原型作用域。@Configuration代表 SpringBoot 项目的配置文件。以下是一段展示如何使用@Scope原型注解的代码:

@Configuration public class ConfigFoo {
   @Bean @Scope("prototype") public Foo foo() {
      return new Foo();
   }
}

Spring MVC

Spring Web MVC 框架使用模型-视图-控制器MVC)架构来管理 Web 应用程序。这为开发者提供了一个现成的组件,可以用来开发强大且松散耦合的 Web 应用程序。随着 Spring 3.0 的推出,@Controller组件还通过@PathVariable注解和其他特性,使您能够创建和平的 Web 区域和应用程序。MVC 模式将应用程序的不同方面分开,如输入、业务和 UI 逻辑。

MVC 有三个部分:

  • 模型是 MVC 应用程序的核心。这是产生构成应用程序核心有用性的主要逻辑和信息对象的地方。

  • 视图是模型提供的信息被引入客户端的地方。视图调节视觉(或其他)界面组件——它选择、过滤和安排模型提供的数据。

  • 控制器负责准备客户端请求,构建适当的模型,并将其传递给视图进行渲染。

以下是 Spring MVC 框架的一些优点:

  • Spring MVC 有助于分离每个角色,如模型对象和控制器。

  • 在开发和部署应用程序时,它帮助开发者使用轻量级 servlet 容器。

  • 它为项目提供了一个强大且灵活的配置。

  • 您可以非常快速且并行地开发一个项目。

  • 测试非常简单,您可以使用 setter 函数注入测试数据。

DispatcherServlet

DispatcherServlet 是 Spring MVC 的核心组件之一。它在应用程序中充当前端控制器。前端控制器意味着 Spring MVC 接收所有传入的请求并将它们转发到 Spring MVC 控制器进行处理。这与 Spring IoC 容器完全协调,因此您可以使用 Spring 的每个元素。

DispatcherServlet 处理所有在 Spring MVC 下设计的 HTTP 请求和响应。

下面是一个用于说明 DispatcherServlet 的图示:

图片

与即将到来的 HTTP 请求到 DispatcherServlet 相关的事件顺序如下:

  1. 应用程序(作为客户端)向 DispatcherServlet 发送请求。

  2. DispatcherServlet 会请求相关的 Handler Mapping 来调用 Controller

  3. ControllerDispatcherServlet 接收请求,并根据 GETPOST 方法调用相关的服务函数。服务函数根据业务逻辑设置模型数据。

  4. ViewResolver 选择定义好的 View

  5. 定义好的 View 在应用程序中执行。

创建项目

现在,我们将学习如何使用 Kotlin 了解 MVC 框架。尽管这个项目是一个 Web 应用程序,我们需要使用 Maven 进行依赖管理,但我们首先需要创建一个动态 Web 应用程序,然后将其转换为 Maven 项目。以下截图展示了如何准备我们的任务骨架结构:

图片

现在我们将学习如何将此项目转换为 Maven 项目。

转换为 Maven 项目

现在我们已经准备好了我们的 Maven Web 应用程序项目的骨架代码,我们可以开始对其进行改进,同时创建我们的 Spring MVC HELLO WORLD 应用程序。

创建的项目是一个非 Maven 项目。我们需要将项目转换为 Maven 项目。

要将此项目转换为 Maven 项目,打开现有项目。在项目工具窗口中,右键单击您的项目并选择添加框架支持。

在打开的对话框中,从左侧选项中选择 Maven 并点击确定:

图片

将 Spring MVC 依赖项添加到 pom.xml

我们必须在 pom.xml 中包含 spring-webspring**-**webmvc 依赖项,以及包括一个 servlet 编程接口、JSP 编程接口和 JSTL 依赖项。以下是我们的项目 pom.xml 文件的部分内容(完整版本在 GitHub 上),其中包含 Spring CoreKotlinWeb 依赖项:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         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>
     ----
     ----
    <properties>
 <springframework.version>5.0.8.RELEASE</springframework.version>
        <kotlin.version>1.3.0</kotlin.version>
        <jstl.version>1.2</jstl.version>
    </properties>

    <dependencies>
 <!--Spring dependencies-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
        </dependency>
        ----
        ----
        ----

 <!--We need to add the following Kotlin dependencies-->
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jdk8</artifactId>
        </dependency>
    </dependencies>

    <build>
    <plugins>
        ----
        ----
    </plugins>
    </build>
</project>

创建 Spring 配置 Bean

前往 /WebContent/WEB-INF/ 目录并创建一个名为 spring-mvc-kotlin-servlet.xml 的 XML 文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <mvc:annotation-driven />
 <context:component-scan
            base-package="mvckotlin" />
    <mvc:default-servlet-handler />

    <bean id="viewResolver"
          class="org.springframework.web.servlet.view.UrlBasedViewResolver">
        <property name="viewClass"
                  value="org.springframework.web.servlet.view.JstlView" />
        <property name="prefix" value="/WEB-INF/jsp/" />
        <property name="suffix" value=".jsp" />
    </bean>
</beans>

spring-mvc-kotlin-servlet.xml 配置文件中,我们提到了 <context:component-scan> 标签。现在,Spring 将加载来自 mvckotlin 包及其所有子包的所有组件:

  • 这将加载我们的 MVCKotlinApp.class 并分配一个 viewResolver 实例。

  • <property name="prefix" value="/WEB-INF/jsp/" /> 将解析视图并添加一个名为 /WEB-INF/jsp/ 的前缀字符串。

  • 注意,我们在 MVCKotlinApp 类中返回了一个名为 welcome 的视图 ModelAndView 对象。

  • 这将被解析为 /WEB-INF/jsp/greeting.jsp 路径。

  • /WebContent/WEB-INF/ 目录下有一个 web.xml 文件。如果您找不到它,请在 /WebContent/WEB-INF/ 目录中创建它。以下是 web.xml 的一部分代码:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <display-name>spring-mvc-kotlin</display-name>
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
        <welcome-file>default.jsp</welcome-file>
        <welcome-file>default.html</welcome-file>
        <welcome-file>index.html</welcome-file>

    </welcome-file-list>
    <servlet>
 <servlet-name>spring-mvc-kotlin</servlet-name>        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
 <servlet-name>spring-mvc-kotlin</servlet-name>        <url-pattern>/index.jsp</url-pattern>
        <url-pattern>/greeting.jsp</url-pattern>
    </servlet-mapping>
</web-app>

web.xml 将将 DispatcherServlet 映射到 /greeting.jsp URL 模式。此外,请注意,我们已将 index.jsp 作为问候文件提及。

初始化后,DispatcherServlet 将在 WEB-INF 文件夹中查找名为 [servlet-name]-servlet.xml 的文件。Servlet XML 文件的前缀名称和 web.xml<servlet-name> 标签的值必须相同。在我们的示例中,servlet 的名称是 spring-mvc-kotlin-servlet.xml

创建控制器类

在项目中转到 src | main | java,创建我们在 spring-mvc-kotlin-servlet.xml 中提到的包名。假设我们的包名是 mvckotlin

图片

创建一个控制器 .kt 文件。我们称这个为 MVCKotlinAppController.kt

@Controller class MVCKotlinAppController {
    @RequestMapping("/greeting")
    fun greetingMessage(): ModelAndView {
        val message =
            "<div style='text-align:center;'>" +
                "<h3>Welcome to Learn Spring for Android Application Development</h3>" +
            "</div>"
        return ModelAndView("greeting", "message", message)
    }
}

我们有一个名为 MVCKotlinAppController.kt 的类,并使用 @Controller 注解,这意味着这个类是一个控制器类。在初始化项目后,Spring 从这里开始搜索包。

@RequestMapping("/greeting") 注解将映射一个网络请求,/greeting 将创建一个基本 URI。

我们创建了一个名为 greetingMessage() 的函数,它将返回一个 ModelAndView 对象。在这里,我们只是创建了一个用于问候的示例 HTML 代码。如果我们访问 http://localhost:8080/greeting,这将基于 greetingMessage() 返回一个视图。

视图

创建一个名为 /WebContent/index.jsp 的新文件,内容如下:

<%@ page contentType="text/html;charset=UTF-8" language="kotlin" %>
<html>
<head>
    <title>Spring MVC Kotlin</title>
</head>
<body>
<br>
<div style="text-align: center">
    <h2>
        Hey You..!! This is your 1st Spring MCV Tutorial..<br> <br>
    </h2>
    <h3>
        <a href="greeting.html">Click here to See Welcome Message... </a>(to
        check Spring MVC Controller... @RequestMapping("/greeting"))
    </h3>
</div>
</body>
</html>

然后创建另一个名为 /WebContent/WEB-INF/jsp/greeting.jsp 的文件,内容如下:

<html>
<head>
    <title>Spring MVC Kotlin</title>
</head>
<body>
${message}
</body>
</html>

IntelliJ Ultimate

要运行项目,您需要设置运行配置。按照以下步骤操作:

  1. 从工具栏中点击 Run... 按钮,然后添加带有 clean install 注释的 Maven:

图片

  1. 添加 TomCat Server --> Local 并将 SpringMVCKotlin:war 构建从部署添加:

图片

  1. 点击菜单栏上的运行按钮以启动项目。

Eclipse

构建项目的步骤如下:

  1. 要运行项目,请右键单击项目 | 运行 As | Maven Build....

  2. 添加目标—干净安装。***

  3. 点击应用并运行。

如果没有错误,您将看到以下 BUILD SUCCESS 消息:

图片

访问 http://localhost:8080/SpringMVCKotlin/,您将看到以下演示代码的输出:

图片

SpringBoot

SpringBoot是 Spring 框架的一个模块,它具有一些帮助开发者创建生产级应用程序的功能。SpringBoot 由两个词组成——BOOT来自 bootstrap,而SPRING是用于构建 Java 企业应用程序的框架。这是一个大型框架,也受到许多其他框架的支持。SpringBoot 与之相似,因为它允许您从头开始引导 Spring 应用程序,这就是它得名 SpringBoot 的原因。根据spring.io,以下是 SpringBoot 的定义——“Spring Boot 使创建独立、生产级、基于 Spring 的应用程序变得容易,您可以直接运行。”这意味着它帮助您在没有他人帮助的情况下创建可运行的项目。此外,我们在这里展示了一个生产级的项目,这是一个成品应用程序。SpringBoot 最小化了设置应用程序的痛苦。

SpringBoot 的功能如下:

  • 它有助于创建独立的 Spring 应用程序。

  • 它自带 Tomcat、Jetty 或 Undertow,因此无需担心设置服务器环境。

  • 使用 SpringBoot,您不需要部署 WAR 文件。

  • 可以自动导入第三方框架及其配置。

  • 如果您使用 SpringBoot,则不需要 XML 配置。

SpringBoot 不会生成代码或更改您的文件。相反,当您启动应用程序时,SpringBoot 会动态连接 beans 和设置,并将它们应用到应用程序上下文中。

让我们创建一个 SpringBoot 项目来了解其依赖项和功能。

创建项目

要创建一个 Spring Boot 项目,让我们从start.spring.io/生成一个示例项目。在这里,您可以添加所需的依赖项,例如WebThymeleafJPADevTools。可以按照以下步骤操作:

  1. 在顶部的下拉菜单中,选择Maven Project,带有Kotlin和 Spring Boot 2.1.1 (SNAPSHOT)

图片

  1. 输入工件包名,并添加依赖项。然后点击生成项目。

  2. 下载并解压项目。

  3. 将下载的项目导入到您的 IDE 中。

按照这些步骤操作后,您就可以使用和修改项目了。让我们看看这个项目里面有什么。您将在src/main/kotlin/{packageName}/AppController.kt下找到一个控制器文件。

这里是controller文件中的一段代码:

@RestController
class HtmlController {
 @GetMapping("/")
    fun blog(model: Model): String {
        model["title"] = "Greeting"
        return "index"
    }
}

创建一个名为HtmlController.kt的类,并用@RestController注解使其成为一个控制器类,我们将处理网络请求。@RestController@Controller@ResponseBody的组合。

创建一个名为blog(model: Model)的函数,并用@GetMappingmaps("/")注解它。这将返回index.xml作为输出。

创建应用程序类

**src/main/kotlin/{packageName}** 下创建一个名为 SpringBootKotlinApplication.kt 的应用程序类:

@SpringBootApplication class SpringBootKotlinApplication

fun main(args: Array<String>) {
   runApplication<SpringBootKotlinApplication>(*args)
}

@SpringBootApplication 用于启用以下三个功能:

  • @Configuration 启用了基于 Java 的配置。

  • @EnableAutoConfiguration 启用了 SpringBoot 的自动配置功能。

  • @ComponentScan 启用了组件扫描。

main() 函数使用 SpringBoot 的 SpringApplication.run() 方法来分发应用程序。这个网络应用程序是 100% 纯 Kotlin,这里不需要安排任何管道或基础结构。

同样,有一个作为 @Bean 设置的 CommandLineRunner 函数,它在启动时持续运行。它恢复由你的应用程序创建的或由 SpringBoot 自动添加的所有 bean。然后它对这些进行排序并打印出来。

SpringBootKotlinApplication 类的代码中,与 Java 相比,你可以看到没有分号,空类中没有部分(如果你需要通过 @Bean 注解声明 beans,可以添加一些),以及使用 runApplication 顶级函数。runApplication<SpringBootKotlinApplication>(*args) 是 Kotlin 的非正式选项,与 SpringApplication.run(SpringBootKotlinApplication::class.java, *args) 相比,并且可以用来自定义应用程序。

现在,在 src/main/resources/templates/ 下的文件夹中创建一个 HTML 文件。

index.html 的内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>Spring Boot Kotlin</title>
</head>
<body>
   <p>Welcome, Naruto. This project is based on Spring Boot in Kotlin</p>
</body>
</html>

通过运行 SpringBootKotlinApplication.kt 中的 main 函数来启动网络应用程序。如果一切正常,你将在日志中看到以下内容:

接下来,访问 http://localhost:8080/。在这里,你应该看到一个带有 SpringBoot Kotlin 应用程序标题的网页:

我们已经涵盖了 SpringBoot 的基础知识。稍后,我们将通过更多的依赖项对其进行更深入的探讨。

摘要

在本章中,我们探讨了 Spring 及其模块、依赖项和函数的使用。我们试图涵盖本书其余部分所需的所有基本信息。我们研究了 Spring 框架的稳定和坚固的架构,包括核心、信息访问、Web、AOP、仪器和测试。此外,我们了解了 bean 的生命周期以及如何以三种不同的方式设计 bean。我们发现了 bean 配置的深度,并学习了在 XML、注解和代码中使用 bean。现在我们知道了如何将依赖项注入到任务中。

我们探讨了两个值得注意的框架:Spring MVC 和 SpringBoot。现在,我们将能够创建一个基于 MVC 的项目,包括其依赖项和模块。此外,我们还学习了 SpringBoot 的使用,并创建了一个使用 Boot 的 Web 应用程序,使我们能够在没有 HTML 文件的情况下制作网页。我们还探讨了 Spring MVC 和 SpringBoot 之间的区别。现在,您可以使用 Kotlin 语言创建 Spring 项目。

在下一章中,我们将学习构建 Android 平台上的客户端应用程序所需的 Android 和 Spring 模块。

问题

  1. 什么是 Spring 框架?

  2. 什么是依赖注入?

  3. 什么是面向方面的编程?

  4. 什么是 Spring IoC 容器?

  5. 什么是 Spring Bean?

  6. 在 Spring MVC 中,控制器是什么?

  7. 什么是DispatcherServlet

  8. 什么是ContextLoaderListener

  9. 什么是样板代码?

进一步阅读

第四章:Android 的 Spring 模块

本章节将涵盖支持 Spring for Android 的模块和功能,以及在 Android 中使用 REST 作为客户端。有一些模块有助于请求和检索 REST API。它们还提供安全功能,如基本认证OAuth2。由于这些安全措施,服务器资源得到保护,因此难以被黑客攻击。即使客户端也需要从资源所有者那里获得权限才能使用受保护服务器的资源。这些模块还集成了强大的基于 OAuth 的授权客户端和主流社交网站(如 Google、Twitter、Facebook 等)的实现。

本章节涵盖了以下主题:

  • The RestTemplate module.

  • The Gradle and Maven repository

  • RestTemplate模块

  • Retrofit

  • 创建一个 Android 应用

技术要求

开发 Android 应用需要 Android SDK。开发者最初使用 Eclipse 和 Android 插件来开发 Android 应用。但后来,Google 宣布 Android Studio 是 Android 应用开发的官方工具。它包含所有必要的模块,如 Gradle、Maven、Android SDK、NDK、Java JDK 等,因此我们不需要使用终端命令行。在第一章,关于环境中,我们展示了如何使用 Android Studio 下载和创建一个示例 Android 应用。

本章节的示例源代码可在 GitHub 上通过以下链接获取:github.com/PacktPublishing/Learn-Spring-for-Android-Application-Development/tree/master/Chapter04

REST 客户端模块

表示状态转移REST)旨在利用现有协议的优势。REST 的一致性系统通常被称为RESTful 系统。它几乎可以在任何协议上使用,但在使用 Web API 时通常利用 HTTP。这使得系统之间的通信更加简单。这些系统通过它们无状态和分离客户端和服务器关注点的方式被描述。我们将深入探讨这些术语的含义以及为什么它们是 Web 服务有利的特性。

A RESTful web service is responded to with a payload formatted in either HTML, XML, JSON, or some other format. The response can affirm that a change has been made to the requested response, and the reaction can give hypertext links that are related to other resources, or a bundle of resources. At the point in which HTTP is utilized, as is normal, the tasks that are accessible are GET, POST, PUT, DELETE, and other predefined HTTP functions.

要使用 Spring for Android,您可以使用不同的 HTTP 库。Spring 建议使用RestTemplate用于 Android。这现在已经过时,可能不会支持较新的 Android 版本。然而,现在您可以找到一些更简单、更强大且功能丰富的库。您可以使用不同的 HTTP 库,例如以下之一:

  • RestTemplate

  • Retrofit

  • Volley

我们将在本章中探讨所有这些库的使用。在我们的后续章节中,我们将使用 Retrofit,因为它更简单、更新、健壮,并且需要编写的代码更少。然而,您可以在项目中使用任何这些库。

RestTemplate 模块

RestTemplate是一个健壮的基于 Java 的 REST 客户端。在 Android 应用程序开发中,我们可以使用RestTemplate模块,它将提供一个模板来请求和检索 REST API。RestTemplate是 Spring 的同步客户端 HTTP 访问的核心类。它的目的是解开与 HTTP 服务器的通信并授权 RESTful 标准。

RestTemplate是同步 RESTful HTTP 请求的主要类。使用一个本地的 Android HTTP 客户端库来检索请求。默认的ClientHttpRequestFactory,在您创建另一个RestTemplate示例时使用,取决于您的应用程序运行的 Android 版本。

Gradle 和 Maven 仓库

要开发 Android 应用程序,我们必须实现或编译一些依赖项。Android 官方支持 Gradle 来实现或编译依赖项。Android 也支持 Maven,因此如果您想使用 Maven,则需要修改pom.xml

您可以在mvnrepository.com/artifact/org.springframework.android/spring-android-core检查实现spring-android-core的依赖项的最新版本,该依赖项包含了 Android 的核心模块。

您可以在mvnrepository.com/artifact/org.springframework.android/spring-android-rest-template检查实现spring-android-rest-template的依赖项的最新版本,该依赖项包含了所有用于RestTemplate的模块。

现在,我们将探讨在 Android 项目中使用 Gradle 和 Maven 的方法。

Gradle

Gradle 是一个构建系统,用于通过监控条件和提供自定义构建逻辑来构建 Android 包(APK 文件)。它是一个基于 JVM 的框架,这意味着您可以使用 Java 编写自己的内容,Android Studio 正是利用了这一点。

在 Android Studio 中,Gradle 是一个自定义的构建工具,用于通过监控依赖项和提供自定义构建逻辑来构建 Android 包(APK 文件)。APK 文件(Android 应用程序包)是一个特殊格式的压缩文件,包含字节码、资源(图片、UI、XML 等)和清单文件。

实现这些依赖项的依赖命令如下所示:

dependencies {
    // https://mvnrepository.com/artifact/org.springframework.android/spring-android-rest-template
 implementation 'org.springframework.android:spring-android-rest-template:2.0.0.M3' // https://mvnrepository.com/artifact/org.springframework.android/spring-android-core
 implementation 'org.springframework.android:spring-android-core:2.0.0.M3'
}

repositories {
    maven {
        url 'https://repo.spring.io/libs-snapshot'
    }
}

Maven

Android Maven 模块用于构建 Android OS 的应用程序和构建库。这些用于创建 Android Archive LibraryAAR)和继承 APKLIB 格式,从而使用 Apache Maven。

这里是一个如何在 pom.xml 中添加 Android 依赖项的代码示例:

<dependencies>
    <!-- https://mvnrepository.com/artifact/org.springframework.android/spring-android-rest-template -->
 <dependency>
 <groupId>org.springframework.android</groupId>
 <artifactId>spring-android-rest-template</artifactId>
 <version>2.0.0.BUILD-SNAPSHOT</version>
 </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.android/spring-android-core -->
 <dependency>
         <groupId>org.springframework.android</groupId>
         <artifactId>spring-android-core</artifactId>
         <version>1.0.1.RELEASE</version>
     </dependency>
</dependencies>
<repositories>
    <repository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/libs-snapshot</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
</repositories>

RestTemplate 构造函数

以下代码列出了四个 RestTemplate 构造函数:

RestTemplate();
RestTemplate(boolean includeDefaultConverters);
RestTemplate(ClientHttpRequestFactory requestFactory);
RestTemplate(boolean includeDefaultConverters, ClientHttpRequestFactory requestFactory);

此构造函数默认没有参数。如果您想使用另一个 RestTemplate 示例中的默认消息转换器集,可以将 TRUE 作为参数传递。如果您想使用另一个 ClientHttpRequestFactory,则需要将其作为参数传递。

RestTemplate 函数

RestTemplate 提供了大量的函数。它有六个主要的 HTTP 函数,这使得构建多个 RESTful 服务和授权 REST 最佳实践变得简单。RestTemplate 的策略名称遵循一个命名传统;前缀部分展示了 HTTP 策略是什么,第二部分展示了将返回什么。在 RestTemplate 中有一个名为 ResponseErrorHandler 的接口,用于确定特定响应是否有错误。以下是六个 HTTP 函数的描述。

HTTP GET

HTTP 定义了一组请求函数,以展示针对给定资源的期望执行的活动。GET 函数请求预定资源的描述,并要求使用 GET 仅检索数据。GET 是最著名的 HTTP 函数之一。

这里是 HTTP GET 的常见函数:

@Throws(RestClientException::class)
fun <T> getForObject(url: String, responseType: Class<T>, vararg urlVariables: Any): T

@Throws(RestClientException::class)
fun <T> getForObject(url: String, responseType: Class<T>, urlVariables: Map<String, *>): T

@Throws(RestClientException::class)
fun <T> getForObject(url: URI, responseType: Class<T>): T

fun <T> getForEntity(url: String, responseType: Class<T>, vararg urlVariables: Any): ResponseEntity<T>

fun <T> getForEntity(url: String, responseType: Class<T>, urlVariables: Map<String, *>): ResponseEntity<T>

@Throws(RestClientException::class)
fun <T> getForEntity(url: URI, responseType: Class<T>): ResponseEntity<T>

这里是一个如何调用这些函数的示例:


val restTemplate = RestTemplate()

val baseUrl: String ?= "YOUR_URL" // API URL as String
val response = restTemplate.getForEntity(baseUrl, String::class.java)

val uri = URI(baseUrl) // API URL as URL format
val responseURI = restTemplate.getForEntity(uri, String::class.java)Auth Module

HTTP POST

HTTP POST 请求 URI 上的资产执行给定的操作。POST 通常用于创建新内容;然而,它也可以用于更新元素。

这里是 HTTP POST 的常见函数:

@Throws(RestClientException::class)
fun postForLocation(url: String, request: Any, vararg urlVariables: Any): URI

fun postForLocation(url: String, request: Any, urlVariables: Map<String, *>): URI

@Throws(RestClientException::class)
fun postForLocation(url: URI, request: Any): URI

fun <T> postForObject(url: String, request: Any, responseType: Class<T>, vararg uriVariables: Any): T

fun <T> postForObject(url: String, request: Any, responseType: Class<T>, uriVariables: Map<String, *>): T

@Throws(RestClientException::class)
fun <T> postForObject(url: URI, request: Any, responseType: Class<T>): T

fun <T> postForEntity(url: String, request: Any, responseType: Class<T>, vararg uriVariables: Any): ResponseEntity<T>

@Throws(RestClientException::class)
fun <T> postForEntity(url: String, request: Any, responseType: Class<T>, uriVariables: Map<String, *>): ResponseEntity<T>

@Throws(RestClientException::class)
fun <T> postForEntity(url: URI, request: Any, responseType: Class<T>): ResponseEntity<T>

这里是一个如何调用这些函数的示例:

/** POST **/

val restTemplate = RestTemplate()

val baseUrl: String ?= "YOUR_URL"
val uri = URI(baseUrl)
val body = "The Body"

val response = restTemplate.postForEntity(baseUrl, body, String::class.java)

val request = HttpEntity(body)
val responseExchange = restTemplate.exchange(baseUrl, HttpMethod.POST, request, String::class.java)

val responseURI = restTemplate.postForEntity(uri, body, String::class.java)
val responseExchangeURI = restTemplate.exchange(uri, HttpMethod.POST, request, String::class.java)

HTTP PUT

要在 URI 中存储一个元素,PUT 函数可以创建一个新的元素或更新现有的一个。PUT 请求是幂等的。幂等性是 PUT 请求与 POST 请求的基本区别。

这里是 HTTP PUT 的常见函数:

Here are the common functions -
@Throws(RestClientException::class)
fun put(url: String, request: Any, vararg urlVariables: Any)

@Throws(RestClientException::class)
fun put(url: String, request: Any, urlVariables: Map<String, *>)

@Throws(RestClientException::class)
fun put(url: String, request: Any, urlVariables: Map<String, *>)

这里是一个如何调用 HTTP PUT 函数的示例:

val baseUrl: String ?= "YOUR_URL"
val restTemplate = RestTemplate()
val uri = URI(baseUrl)

val body = "The Body"

restTemplate.put(baseUrl, body)
restTemplate.put(uri, body)

HTTP DELETE

HTTP DELETE 是一个用于删除资源的请求函数。然而,资源不必立即删除。DELETE 可以是异步的或长时间运行的请求。

这里是 HTTP DELETE 的常见函数:

@Throws(RestClientException::class)
fun delete(url: String, vararg urlVariables: Any)

@Throws(RestClientException::class)
fun delete(url: String, urlVariables: Map<String, *>)

@Throws(RestClientException::class)
fun delete(url: URI) 

这里是一个如何调用这些函数的示例:

val baseUrl: String ?= "YOUR_URL"
val restTemplate = RestTemplate()
val uri = URI(baseUrl)

restTemplate.delete(baseUrl)
restTemplate.delete(uri)

HTTP OPTIONS

HTTP OPTIONS 函数用于描述目标资源的通信选项。客户端可以指定 OPTIONS 方法的 URL,或者使用参考标记 (*) 来引用整个服务器。

这里是 HTTP OPTIONS 的常见功能:

@Throws(RestClientException::class)
fun optionsForAllow(url: String, vararg urlVariables: Any): Set<HttpMethod>

@Throws(RestClientException::class)
fun optionsForAllow(url: String, urlVariables: Map<String, *>): Set<HttpMethod>

@Throws(RestClientException::class)
fun optionsForAllow(url: URI): Set<HttpMethod>

这里是如何调用函数的示例:

val baseUrl: String ?= "YOUR_URL"
val restTemplate = RestTemplate()
val allowHeaders = restTemplate.optionsForAllow(baseUrl)

val uri = URI(baseUrl)
val allowHeadersURI = restTemplate.optionsForAllow(uri)

HTTP HEAD

在当前版本的 Spring(4.3.10)中,HEAD 被自动支持。

映射到 GET@RequestMapping 函数也隐式映射到 HEAD,这意味着不需要显式声明 HEAD。HTTP HEAD 请求的处理方式就像 HTTP GET 一样,但不同的是,不写入主体,只计算字节数以及 Content-Length 头。

这里是 HTTP HEAD 的常见功能:

@Throws(RestClientException::class)
fun headForHeaders(url: String, vararg urlVariables: Any): HttpHeaders

@Throws(RestClientException::class)
fun headForHeaders(url: String, urlVariables: Map<String, *>): HttpHeaders

@Throws(RestClientException::class)
fun headForHeaders(url: URI): HttpHeaders

Retrofit

Retrofit 是一个库,它使解析 API 响应变得简单且易于在应用程序中使用。Retrofit 是一个 Java 和 Android 的 REST 客户端,它通过基于 REST 的网络服务使恢复和传输 JSON 变得相对简单。在 Retrofit 中,你可以安排使用哪个转换器进行数据序列化。通常,对于 JSON,你使用 Gson,但你可以添加自定义转换器来处理 XML 或其他格式。Retrofit 使用 OkHttp 库进行 HTTP 请求。

Retrofit 的使用

要与 Retrofit 一起使用,你需要以下三个类:

  • 一个模型类,用作 JSON 模型

  • 定义可能 HTTP 请求的接口

  • Retrofit.Builder 类,它使用接口和开发者编程接口来允许指定 HTTP 请求的 URL 端点。

接口中的每个函数代表一个可能的编程接口调用。它必须有一个 HTTP 注解(GETPOSTDELETE 等)来指定请求类型和相对 URL。

Retrofit 的优势

Retrofit 非常容易使用。它基本上给你一个机会将编程接口调用视为简单的 Java 方法调用,因此你只需指定要访问的 URL 和请求/响应参数作为 Java 类。

整个系统调用,加上 JSON/XML 解析,完全由 Retrofit(在 Gson 的帮助下进行 JSON 解析)处理,同时支持可插拔的序列化和反序列化格式。

配置 Retrofit

当然,Retrofit 可以将 HTTP 主体反序列化为 OkHttpResponseBody 类型,并且它可以接受其 RequestBody 类型用于 @Body

可以添加转换器以支持不同的类型。七种模块调整主流序列化库以供您使用。以下是一些库:

  • Gson: com.squareup.retrofit2:converter-gson

  • Jackson: com.squareup.retrofit2:converter-jackson

  • Moshi: com.squareup.retrofit2:converter-moshi

  • Protobuf: com.squareup.retrofit2:converter-protobuf

  • Wire: com.squareup.retrofit2:converter-wire

  • Simple XML: com.squareup.retrofit2:converter-simplexml

  • Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars

下载 Retrofit

search.maven.org/remote_content?g=com.squareup.retrofit2&a=retrofit&v=LATEST 下载最新的 JAR 文件。

或者,您可以使用以下代码通过 Maven 注入依赖项:

<dependency>
    <groupId>com.squareup.retrofit2</groupId>
    <artifactId>retrofit</artifactId>
    <version>2.4.0</version>
</dependency>

或者,您可以使用以下代码使用 Gradle:

implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0'

HTTP 请求函数

每个函数都必须有一个 HTTP 注解,该注解指定请求函数和相对 URL。有五个内置注解——GETPOSTPUTDELETEHEAD。资产的总体 URL 由注解指示。

让我们看看这些注解的使用。我们考虑的是基于 GitHub API v3 的所有 URL (developer.github.com/v3/)。

GET

假设您想从您的 GitHub 账户获取详细信息响应。您需要使用以下端点以及 @GET 函数来获取用户信息:

@GET("group/{id}/users") Call<List<Users>> groupList(@Path("id") int id);

假设您想在您的 GitHub 账户中创建一个新的仓库。在这里,您需要使用以下端点以及 @POST 函数:

@POST("user/repos")
fun createRepo(@Body repo:Repository, 
               @Header("Authorization") accessToken: String,
               @Header("Accept") apiVersionSpec: String,
               @Header("Content-Type") contentType: String): Call<Repository>

PUT

假设您想更新 GitHub Gist 对象。您需要使用以下端点以及 @PUT 函数:

@PUT("gists/{id}")
fun updateGist(@Path("id") id: String, 
               @Body gist: Gist): Call<ResponseBody>

DELETE

假设您想从您的 GitHub 账户删除一个仓库。在这种情况下,您需要使用以下端点以及 @DELETE 函数:

@DELETE("repos/{owner}/{repo}")
    fun deleteRepo(@Header("Authorization") accessToken: String,
 @Header("Accept") apiVersionSpec: String,
 @Path("repo") repo: String,
 @Path("owner") owner: String): Call<DeleteRepos>

HEAD

可以使用 @Header 注解逐步刷新请求头。如果值无效,则忽略该头:

// example one
@GET("user")
Call<User> getUser(@Header("Authorization") String authorization)

// example two
@Headers("Accept: application/vnd.github.v3.full+json", "User-Agent: Spring for Android")
@GET("users/{username}")
fun getUser(@Path("username") username: String): Call<Users>

创建 Android 应用

让我们创建一个简单的 Android 应用程序作为客户端,该客户端将使用 GitHub API 获取 REST API。首先,我们需要在 Android Studio 中创建一个应用程序,并写下我们的项目和公司域名。别忘了勾选包含 Kotlin 支持。这将包括 Kotlin 的所有支持。以下截图显示了创建 Android 项目窗口:

然后,从手机和平板电脑选项中选择最低 API 版本。对于此项目,无需添加其他选项。点击下一步后,在添加到移动部分,您可以选择空活动,然后重命名活动名称和布局,点击完成。构建完成后,您就可以开始创建 Android 应用了。

以下截图显示了此项目的最终文件:

Gradle 信息

这里是我的 Android Studio 的 Gradle 文件详情:

buildscript {
 ext.kotlin_version = '1.3.10'    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

Gradle 依赖项

我们将使用 Retrofit 及其功能,因此需要实现所有依赖项,如下代码所示:

    implementation 'com.squareup.retrofit2:retrofit:2.4.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.4.0'

    implementation 'com.squareup.retrofit2:retrofit-converters:2.5.0'
    implementation 'com.squareup.retrofit2:retrofit-adapters:2.5.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:3.12.0'
    implementation 'com.google.code.gson:gson:2.8.5'

创建模型

我们将使用 GitHub API。你可以检查所有 REST API URL 在api.github.com/。我们将使用最简单的 API,它没有安全问题。我们将显示用户仓库的列表。API 是api.github.com/users/{user}/repos。你需要一个带有用户名的GETHTTP 函数。

以下截图显示了 REST API 的输出:

上一张截图的左侧显示了仓库内容的一部分,右侧是折叠的仓库总列表。

因此,根据 API,我们将为客户端创建一个用户模型。这里是一个名为GitHubUserModel.kt的模型类,我们将只显示所有仓库列表的名称:

class GitHubUserModel {
 val name: String? = null }

创建一个将包含 HTTP 请求函数的接口。在这个项目中,我们只会使用一个GET函数来检索所有用户的详细信息。在这里,我们使用GETRetrofit 注解来编码有关参数和请求函数的详细信息。对于这个函数,我们的端点是/users/{user}/repos,你需要添加一个userName参数,它将提供一个UserModel列表。

这里是GithubService接口的代码:

interface GithubService {
 @GET("/users/{user}/repos")
    fun reposOfUser(@Path("user") user: String): Call<List<GitHubUserModel>>
}

实现服务

这个类负责主要任务。它将负责使用Retrofit.builder类控制所有任务,并将其配置为给定 URL 的基础。

这里是**UserServiceImpl.kt**的代码:

class GithubServiceImpl{
   fun getGithubServiceFactory(): GithubService {
        val retrofit = Retrofit.Builder()
                .baseUrl("https://api.github.com/")
                .addConverterFactory(GsonConverterFactory.create())
                .build()
        return retrofit.create(GithubService::class.java)
    }
}

在这里,我们的baseUrl()https://api.github.com/

调用回调

这里,我们是从MainActivity调用CallBack<>。这个回调将包含 REST API 请求的响应。

让我们检查MainActivity.kt代码:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val githubService: GithubService = GithubServiceImpl().getGithubServiceFactory()

        val call: Call<List<GitHubUserModel>> = githubService.reposOfUser("sunnat629")
        call.enqueue(object: Callback<List<GitHubUserModel>>{
            override fun onFailure(call: Call<List<GitHubUserModel>>, t: Throwable) {
                Log.wtf("PACKTPUB", t.message)
            }

            override fun onResponse(call: Call<List<GitHubUserModel>>, response: Response<List<GitHubUserModel>>) {
                val listItems = arrayOfNulls<String>( response.body()!!.size)
                for (i in 0 until response.body()!!.size) {
                    val recipe = response.body()!![i]
                    listItems[i] = recipe.name
                }
                val adapter = ArrayAdapter<String>(this@MainActivity, android.R.layout.simple_list_item_1, listItems)
                displayList.adapter = adapter
            }
        })
    }
}

首先,我们需要初始化GithubServiceImpl().getGithubServiceImpl(username,password)以便我们可以从UserService调用reposOfUser()。在这里,我在参数中添加了我的 GitHub 用户名。然后,我们将调用enqueue(retrofit2.Callback<T>),这将异步执行并发送请求以获取响应。它有两个函数——onResponse()onFailure()。如果有任何与服务器相关的错误,它将调用onFailure(),如果它收到响应和资源,它将调用onResponse()。我们可以使用onResponse()函数的资源。

这里,我们将获取UserModel列表的响应。因此,我们可以使用这个列表在我们的应用程序 UI 中显示 REST 输出。

创建一个接口

我们将显示用户的详细信息以及所有仓库的名称。在这里,我们将使用ListView

这里是acitivity_main.xml文件的代码:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ListView
        android:id="@+id/displayList"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

我们将在MainActivityonResponse()函数中使用这个listview

我们将获取列表并创建一个自定义适配器来显示用户列表,如下面的代码所示:

val listItems = arrayOfNulls<String>( response.body()!!.size)
for (i in 0 until response.body()!!.size) {
    val recipe = response.body()!![i]
    listItems[i] = recipe.name
}
val adapter = ArrayAdapter<String>(this@MainActivity, android.R.layout.simple_list_item_1, listItems)
displayList.adapter = adapter

在这里,我们获取仓库列表并将它们转换为数组。然后,我们使用val adapter = ArrayAdapter<String>(this@MainActivity, android.R.layout.simple_list_item_1, listItems)创建列表的原生适配器,并在我们的列表中使用displayList.adapter = adapter设置适配器。

你永远不应该在主线程上执行长时间运行的任务。这将导致出现应用程序无响应ANR)消息。

移动应用

所以,在一切完成后,运行你的服务器。然后,运行你的应用。以下截图显示了我们的应用输出:

你可以随意修改,但要注意端点和模型。

摘要

在本章中,我们简要介绍了驱动 REST 和 REST 客户端模块的思想。RESTful HTTP 处理未公开功能的方式是独特的。我们看到了不同的 REST 客户端函数库。首先,我们看到了 RestTemplate 是什么以及它在 Android 应用中的实现。现在,我们了解了 RestTemplate 的构造函数及其功能。此外,我们还学习了 Retrofit,使我们能够在 Android 应用中实现 Retrofit。我们还看到了其功能的利用。最后,我们探讨了如何实现 Retrofit 以从 REST API 获取数据。

在接下来的章节中,我们将开发一个完整的项目,包括安全、授权/身份验证、数据库和自定义 REST API,使用 Spring 和 Android 应用作为客户端来处理 API。在这些章节中,你将探索 API 的完整使用,并准备了解如何为服务器创建 API 并从客户端恢复它。

问题

  1. REST 和 RESTful 有什么区别?

  2. 创建 Web API 的架构风格是什么?

  3. 测试你的 Web API 需要哪些工具?

  4. 什么是 RESTful Web 服务?

  5. 什么是 URI?URI 在基于 REST 的 Web 服务中有什么作用?

  6. HTTP 状态码200表示什么?

  7. HTTP 状态码404表示什么?

进一步阅读

第五章:使用 Spring Security 保护应用程序

安全性是企业、电子商务和银行项目中的首要任务之一。这些项目需要创建一个安全系统,因为它们交换数百万美元并存储组织的受保护资源。

Spring Security 是庞大的 Spring 框架系列的一个子任务。它已被升级以与 Spring MVC Web 应用程序框架一起使用,但同样也可以与 Java servlets 一起使用。这支持与一系列其他技术的认证集成,例如轻量级目录访问协议LDAP)、Java 认证和授权服务JAAS)和 OpenID。它被开发为一个针对基于 Java 的企业环境的完整安全解决方案。

在本章中,我们将了解 Spring Security 及其模块,并学习如何在基于 Spring 的项目中实现安全性。本章将涵盖以下主题:

  • Spring Security 架构

  • Spring Security 的优势

  • Spring Security 特性

  • Spring Security 模块

  • 实施 Spring Security

  • 使用 Spring Security 基本认证保护 REST

  • 使用 Spring Security OAuth2 保护 REST

技术要求

您需要添加以下依赖项以启用和使用 Spring Security 的功能。以下是需要添加到 Spring 项目的pom.xml文件中的依赖项:

<dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>Spring_Security_SUB_Module_Name</artifactId>
   <version>CURRENT_RELEASE_VERSION</version>
</dependency>

<dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-core</artifactId>
   <version>5.1.1.RELEASE</version>
</dependency>

您可以在 GitHub 上找到本章的所有示例:github.com/PacktPublishing/Learn-Spring-for-Android-Application-Development/tree/master/Chapter05

Spring Security 架构

Spring Security 是基于 J2EE 的企业生产的安全服务解决方案。它通过使用其特定的依赖注入原则,帮助以更快、更简单的方式开发安全应用程序。为了开发基于 J2EE 的企业应用程序,Spring Security 是一个强大且灵活的认证和授权框架。认证是检查过程或客户端身份的过程。另一方面,授权意味着检查客户端在应用程序中执行活动的权限。

认证

认证是根据用户的用户名和密码识别用户或客户端的过程。它帮助用户根据其身份获取受保护系统对象的访问权限。对于认证过程,Spring Security 为我们提供了AuthenticationManager接口。此接口只有一个功能,名为validate()

以下代码片段是AuthenticationManager接口的一个示例:

interface AuthenticationManager {
 @Throws(AuthenticationException::class)
 fun authenticate(authentication: Authentication): Authentication
} 

在此AuthenticationManager接口的authenticate()中完成了三个任务:

  • 如果其能力可以检查输入代表一个有效的主体,则authenticate()返回Authentication。前面提到的代码通常返回authenticated=true

  • 如果能力发现输入不符合有效规则,它将抛出AuthenticationException

  • 如果能力无法选择任何内容,它将返回null

AuthenticationException是一个运行时异常。应用程序以传统方式处理这个异常。

ProviderManager常用于实现AuthenticationManager,代表一系列AuthenticationProvider对象。如果没有可访问的父级,它将抛出AuthenticationException

AuthenticationProvider类似于AuthenticationManager,但有一个额外的功能。这个额外的功能使客户端能够在支持给定的Authentication类型时进行查询。

这里是AuthenticationProvider接口的一些代码:

interface AuthenticationProvider {
 @Throws(AuthenticationException::class)
    fun authenticate(authentication:Authentication):Authentication
    fun supports(authentication: Class<*>): Boolean
}

这个接口有两个功能——authenticate()返回用户的认证详情,而supports()返回一个Boolean,如果认证和给定的用户名-密码对匹配,则返回true,否则返回false

这里是使用ProviderManagerAuthenticationManager层次结构的图示:

图片

根据这个图示,在一个应用中,ProviderManager可能有一组其他的ProviderManager实例,但第一个将作为父级。每个ProviderManager可能拥有多个AuthenticationManager。例如,如果所有网络资源都在相同的路径下,每个组都将拥有自己的专用AuthenticationManager。然而,将只有一个共同的父级,它将作为全局资源,并由这些专用AuthenticationManager实例共享。现在,让我们看看如何修改认证管理器。

修改认证管理器

Spring Security 提供了一些配置助手来设置应用中的认证管理器功能。这将有助于快速获取功能。AuthenticationManagerBuilder有助于修改认证管理器。

这里是一个如何在ApplicationSecurity.kt类中实现AuthenticationManagerBuilder的示例:

class ApplicationSecurity: WebSecurityConfigurerAdapter() {
    @Autowired
 fun initialize(builder: AuthenticationManagerBuilder, dataSource: DataSource){
builder.jdbcAuthentication().dataSource(dataSource).withUser("Sunnat629").password("packtPub").roles("USER")
 }
}

这里,我们为这个应用中的USER角色提供了一个用户名,sunnat629,和一个密码,packtPub

Spring Boot 附带了一个默认的全局AuthenticationManager,它足够安全。你可以通过提供自己的AuthenticationManager bean 来替换它。

授权

授权是接受或拒绝访问网络资源的过程。它将授予访问使用资源中的数据。在Authentication过程之后,Authorization过程开始。Authorization用于处理访问控制。AccessDecisionManager是这个过程中的核心实体之一。

网络安全

Spring 安全性的 servlet 通道提供 Web 安全性。使用@WebSecurityConfigurer注解启用 Web 安全性,并在 Web 安全性类中覆盖WebSecurityConfigurerAdapter

方法安全

这是一个由 Spring Security 提供的安全方法模块。我们可以在特定功能中提供一个角色,以便基于角色的用户可以访问该功能。

以下注解用于启用此功能:

 @EnableGlobalMethodSecurity(securedEnabled = true)

下面是一个如何在SpringSecurityApplication.kt类中启用方法安全的示例,这是我们的演示项目的主体应用程序类:

@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
class SpringSecurityApplication{

    fun main(args: Array<String>) {
        runApplication<SpringSecurityApplication>(*args)
    }
}

现在,您可以创建方法资源,如下面的代码所示:

@Secured class CustomService{
    @Secured
    fun secure(): String{
 return "The is Secured..."
    }
}

在这里,我们使用@Secured注解创建了一个名为CustomService的安全类,然后创建了一个将返回 Spring 的安全函数。@Secured注解用于指定函数上的角色列表。

Spring Security 的优势

Spring 安全框架提供了以下优势:

  • Spring Security 是一个开源的安全框架

  • 它支持认证和授权

  • 它保护常见任务

  • 它可以与 Spring MVC 和 Servlet API 集成

  • 它支持 Java 和 Kotlin 配置支持

  • 开发和单元测试应用程序很容易

  • Spring 依赖注入和 AOP 可以轻松使用

  • 它开发松散耦合的应用程序

Spring Security 特性

Spring Security 实现了许多功能。

在这里,我们解释了一些常见和主要的功能:

  • LDAP: LDAP 是一个开放的应用协议。它通过互联网维护和访问分布式目录数据服务。

  • OAuth 2.0 登录: 此组件使得客户端能够通过利用他们在 Google、Facebook、Twitter 或 GitHub 上现有的账户来登录应用程序。

  • 基本访问认证: 当客户端通过网络请求时,此方法提供用户名和密码。

  • 摘要访问认证: 这要求程序在通过系统发送个人信息之前确认客户端的身份。

  • Web 表单认证: 在此认证系统中,Web 表单从 Web 浏览器收集和验证用户凭据。

  • 授权: Spring Security 提供此功能,在客户端获取资源之前对其进行批准。

  • HTTP 授权: 这指的是对 Web 请求 URL 的 HTTP 授权。它使用 Apache Ant 路径或正则表达式。

  • 响应式支持: 这提供了响应式编程和 Web 运行时支持。

  • 现代化的密码编码: 从 Spring Security 5.0 引入了一个新的密码编码器,名为DelegatingPasswordEncoder

  • 单点登录: 此功能允许客户端使用单个账户访问多个应用程序。

  • JAAS: JAAS 是一个 Java 中实现的插件式认证模块。

  • 记住我:Spring Security 利用 HTTP cookies,记住客户端的登录 ID 和密码,以便在客户端注销之前避免再次登录。

  • 软件本地化:您可以用任何人类语言创建应用程序的用户界面。

Spring Security 模块

在 Spring Security 3.0 中,Spring Security 模块已被隔离成几个子模块。然而,在当前版本中,有 12 个子模块。为了支持这些模块,代码被细分为独立的容器。这些容器目前是分离的,每个子模块都有不同的有用领域和第三方依赖。

这里是子模块 jar 列表:

  • spring-security-core.jar

  • spring-security-remoting.jar

  • spring-security-web.jar

  • spring-security-config.jar

  • spring-security-ldap.jar

  • spring-security-oauth2-core.jar

  • spring-security-oauth2-client.jar

  • spring-security-oauth2-jose.jar

  • spring-security-acl.jar

  • spring-security-cas.jar

  • spring-security-openid.jar

  • spring-security-test.jar

Spring Security Core 子模块是其他 Security 子模块(如webconfigoauth2)的基础模块。

实现 Spring Security

如果您想在项目中使用 Spring Security,您需要在 Maven 和 Gradle 中实现您想要使用的 Spring Security 依赖项。

让我们看看如何在 Maven 和 Gradle 中实现 Spring Security 依赖项。

Maven

要实现安全依赖项,您需要在pom.xml中实现spring-security-core

<dependency>
 <groupId>org.springframework.security</groupId>
 <artifactId>Spring_Security_SUB_Module_Name</artifactId>
 <version>CURRENT_RELEASE_VERSION</version>
</dependency>

<!--here is an example of a security core sub-modules-->
<dependency>
 <groupId>org.springframework.security</groupId>
 <artifactId>spring-security-core</artifactId>
 <version>5.1.1.RELEASE</version>
</dependency>

Gradle

要实现依赖项,您需要在build.gradle中放入以下代码:

dependencies {
    implementation 'org.springframework.security:[Spring_Security_SUB_Module_Name]:CURRENT_RELEASE_VERSION'
}

// here is an example of a security core sub-modules
dependencies {
 implementation 'org.springframework.security:[spring-security-core]:5.1.1.RELEASE'
}

使用基本认证保护 REST

在这个主题中,我们将通过一个简单的项目学习基本认证。在这里,我们将创建一个示例,您将构建一个安全的 REST API。我们将创建一个项目并实现基本认证。这将帮助我们避免基本配置和完整的 Kotlin 配置时间。对于这个项目,您必须输入用户名和密码才能访问内容。这个项目没有 UI,因此您需要使用 HTTP 客户端来测试项目。在这里,我们使用 Insomnia (insomnia.rest/download/)。您可以从这里测试您的项目并访问内容。

在开始我们的项目之前,我们将了解基本认证及其用途。

什么是基本认证?

基本认证是最简单的认证方案,它是 HTTP 协议内建的。要使用它,客户端需要发送包含认证头部的 HTTP 请求,该头部包含单词Basic后跟一个空格。然后,给定的用户名和密码字符串将被视为username/password并编码为 Base64。例如,如果用户名和密码是Sunnat629pa$$worD,这些将被转换为 Base64 编码,将变为U3VubmF0NjI5L3BhcyQkd29yRA==作为授权。最后,客户端将发送Authorization: Basic U3VubmF0NjI5L3BhcyQkd29yRA==到服务器。

Base64 可以轻松解码。这既不是加密也不是散列。如果你想使用基本认证,我们强烈建议你与其他安全工具一起使用,例如 HTTPS/SSL。

创建项目

我们将创建一个小项目,在这个项目中我们将实现基本认证安全来保护数据。用户需要通过我们的安全系统才能访问数据。让我们按照以下步骤创建项目:

  1. 要创建项目,请访问start.spring.io/并修改给定的字段以满足你的需求。你可以在以下屏幕截图中查看我们的项目信息:

图片

在这里,我们使用Maven Project,选择语言为Kotlin,Spring Boot 版本为2.1.1 (SNAPSHOT)

我们已添加了SecurityWebDevTools依赖项。你可以在pom.xml中查看列表。

  1. 当你选择“生成项目”时,你会以 ZIP 文件的形式获得项目。解压并使用你的 IDE 打开此项目。

  2. 下载和更新 Maven 依赖项需要一点时间。以下是你的项目内容的截图:

图片

如果你需要添加新的依赖项或更新版本,请修改pom.xml。如果你想创建kotlin文件,你需要在src->main->kotlin->{Package_NAME}文件夹下创建文件。

配置 pom.xml

在这个pom.xml中,你将获得有关项目的所有信息。在这里,你可以插入新的依赖项,更新版本等。以下是示例pom.xml(完整代码在 GitHub 上,github.com/PacktPublishing/Learn-Spring-for-Android-Application-Development/tree/master/Chapter05):

<groupId>com.packtpub.sunnat629</groupId> <artifactId>ssbasicauth</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>Spring Security Basic Authentication</name>
<description>A sample project of Spring Security Basic Authentication</description>

----
----

<properties>
   <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
   <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
 <java.version>1.8</java.version>
   <kotlin.version>1.3.0</kotlin.version>
</properties>

<dependencies>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
   </dependency>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
   <dependency>
      <groupId>com.fasterxml.jackson.module</groupId>
      <artifactId>jackson-module-kotlin</artifactId>
   </dependency>
 <dependency>
      <groupId>org.jetbrains.kotlin</groupId>
      <artifactId>kotlin-stdlib-jdk8</artifactId> </dependency>
   <dependency>
      <groupId>org.jetbrains.kotlin</groupId>
      <artifactId>kotlin-reflect</artifactId>
   </dependency>

   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
   </dependency>
   <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-test</artifactId>
      <scope>test</scope>
   </dependency>
</dependencies>

---
---
---

配置 Spring Bean

要配置 Spring Bean,我们将创建一个名为SSBasicAuthApplication.kt的应用程序文件,并使用 Java 配置,它配置 Spring Security 而不需要编写任何 XML 代码。

这是应用程序文件(SSBasicAuthApplication.kt)的简单代码:

@ComponentScan(basePackages = ["com.packtpub.sunnat629.ssbasicauth"])
@SpringBootApplication
class SSBasicAuthApplication: SpringBootServletInitializer()

fun main(args: Array<String>) {
 runApplication<SSBasicAuthApplication>(*args)
}

在这里,我们扩展了SpringBootServletInitializer。这将从传统的WAR存档中运行SpringApplication。此类负责将应用程序上下文中的ServletFilterServletContextInitializer豆绑定到服务器。

@SpringBootApplication是一个便利注解,相当于为SSBasicAuthApplication类声明@Configuration@EnableAutoConfiguration

@ComponentScan注解中提及包名或包名集合,以指定基本包。这与@Configuration注解一起使用,以告诉 Spring 包扫描注解组件。

Spring Security 配置

要为我们的项目添加 Spring Security 配置,请在应用程序包中使用以下代码创建一个名为SSConfig.kt的文件:

@Configuration @EnableWebSecurity
class SSConfig: WebSecurityConfigurerAdapter() {

    @Autowired
    private val authEntryPoint: AuthenticationEntryPoint? = null

    @Throws(Exception::class)
    override fun configure(http: HttpSecurity) {
        http.csrf().disable().authorizeRequests()
                .anyRequest().authenticated()
                .and().httpBasic()
                .authenticationEntryPoint(authEntryPoint)
    }

    @Autowired
    @Throws(Exception::class)
    fun configureGlobal(auth: AuthenticationManagerBuilder) {
        auth.inMemoryAuthentication()
                .withUser("sunnat629")
                .password(PasswordEncoderFactories.createDelegatingPasswordEncoder()
                        .encode("password"))
                .roles("USER")
    }
}

我们使用@Configuration注解了此类,这有助于 Spring 基于注解的配置。@EnableWebSecurity将启用 Spring Security 的 Web 安全支持。

我们扩展了WebSecurityConfigurerAdapter,这将使我们能够覆盖和自定义 Spring 功能。我们使用 HTTP 基本认证,并且所有请求都将使用此方法进行认证。

如果认证失败,我们需要处理这种情况。为此,创建一个名为AuthenticationEntryPoint.kt的认证入口点类并将其autowire。它将帮助在失败的情况下再次尝试此过程。

我们使用用户名sunnat629、密码passwordUSER角色。

配置认证入口点

配置认证入口点以处理失败的认证。当凭证未被授权时,此类主要负责发送响应。

下面是名为AuthenticationEntryPoint.kt的认证入口点类的代码:

@Component
class AuthenticationEntryPoint : BasicAuthenticationEntryPoint() {

    @Throws(IOException::class, ServletException::class)
    override fun commence(request: HttpServletRequest,
                          response: HttpServletResponse,
                          authEx: AuthenticationException) {
        response.addHeader("WWW-Authenticate", "Basic realm=$realmName")
        response.status = HttpServletResponse.SC_UNAUTHORIZED
        val writer = response.writer
        writer.println("HTTP Status 401 - " + authEx.message)
    }

    @Throws(Exception::class)
    override fun afterPropertiesSet() {
        realmName = "packtpub ssbasicauth"
        super.afterPropertiesSet()
    }
}

在这里,我们扩展了BasicAuthenticationEntryPoint()。这将向客户端返回401 Unauthorized响应的完整描述。

401 Unauthorized Error是一个 HTTP 响应状态码。这表示客户端发送的请求无法被认证。

配置 Spring WebApplicationInitializer

Spring WebApplicationInitializer使用 Servlet 3.0+实现来程序化配置ServletContext

下面是WebApplicationInitializer类的示例代码,称为MyApplicationInitializer.kt

class MyApplicationInitializer: WebApplicationInitializer {

    @Throws(ServletException::class)
    override fun onStartup(container: ServletContext) {

        val ctx = AnnotationConfigWebApplicationContext()
        ctx.servletContext = container

        val servlet = container.addServlet("dispatcher", DispatcherServlet(ctx))
        servlet.setLoadOnStartup(1)
        servlet.addMapping("/")
    }
}

本课程将帮助您使用start映射项目 URL 路径"\"。由于我们使用基于代码的注解代替 XML 配置,因此我们使用AnnotationConfigWebApplicationContext

然后,我们创建并注册了分发器 servlet。

创建用户模型

通过访问简单的 REST API,我们创建了一个用户模型类。当客户端输入正确的用户名和密码时,这将返回一些用户详情的简单 JSON 输出。

下面是Users.kt的代码:

class Users(val id: String,
            val name: String,
            val email: String,
            val contactNumber: String)

在这个用户模型中,我们有一个id,一个name,一个email和一个contactNumber。我们将创建一个受我们安全系统保护的 JSON 类型 REST API。

创建控制器

控制器类将映射项目的 URL 路径。在这里,我们将使用GETPOST HTTP请求函数来创建 REST API。以下是项目控制器的一个示例代码,命名为UserController.kt

@RestController
class UserController {

    @GetMapping(path = ["/users"])
    fun userList(): ResponseEntity<List<Users>>{
        return ResponseEntity(getUsers(), HttpStatus.OK)
    }

    private fun getUsers(): List<Users> {
        val user = Users("1","Sunnat", "sunnat123@gmail.com", "0123456789")
        val user1 = Users("2","Chaity", "chaity123@gmail.com", "1234567890")
        val user2 = Users("3","Jisan", "jisan123@gmail.com", "9876543210")
        val user3 = Users("4","Mirza", "mirza123@gmail.com", "5412309876")
        val user4 = Users("5","Hasib", "hasib123@gmail.com", "5678901234")

        return Arrays.asList<Users>(user, user1, user2, user3, user4)
    }
}

在这里,我们使用用户模型创建了一个包含五人的用户列表。在控制器中,@RequestMapping注解应用于类级别和/或方法级别。这会将特定的请求路径映射到控制器。使用@GetMapping(path = ["/users"])注解,如果 HTTP 状态是OK,客户端将发送GET请求以获取用户的列表。

使用 HTTP 客户端

要查看输出,请打开你的第三方 HTTP 客户端工具。在这里,我们使用 Insomnia。

运行项目后,打开 Insomnia。

请按照以下步骤测试项目:

  1. 创建一个带有名称的新请求。

  2. 在 GET 输入框中,输入http://localhost:8080/user URL。在这里,localhost:8080是根 URL,因为我们使用@RequestMapping(path = ["/user"], method = [RequestMethod.GET])在控制器类中,项目将在http://localhost:8080/user路径下运行。

  3. 如果你点击发送按钮,你会看到一个HTTP Status 401 - Bad credentials错误,如下面的截图所示:

虽然你使用的是基本认证,但你必须输入用户名密码才能完成此请求。你需要点击 Auth(第二个标签)并选择Basic认证;你可以在那里输入用户名密码。如果你输入随机的用户名和密码,你也会得到相同的错误。

在输入正确的用户名密码后,你将得到以 JSON 格式输出的用户列表,如下面的截图所示:

你也可以在浏览器中测试。在那里,你会被要求输入用户名密码

你也可以使用浏览器查看 REST API:

在输入用户名和密码后,我们可以看到用户列表:

你已经使用 Spring Security 基本认证创建了一个非常简单的项目。我们希望从现在开始,你可以借助 Spring Security 编写自己的基于认证的项目。

创建 Android 应用程序

是时候创建一个简单的 Android 应用程序作为客户端,从我们的基本认证服务器检索 REST API 了。首先,我们需要在 Android Studio 中创建一个应用程序并填写你的项目名称和公司域名。别忘了勾选Include Kotlin support。以下是创建应用程序项目窗口的截图:

从手机和平板选项中选择最低 API 版本。对于这个项目,不需要添加其他选项。点击下一步后,您可以在 Add an Activity to Mobile 窗口中选择 Empty Activity。在重命名 Activity Namelayout 后,点击完成。构建 gradle 后,您就可以开始创建 Android 应用了。

现在,让我们看看如何在 Gradle 中实现项目的依赖项。

Gradle 信息

在 Gradle 文件中,提及 Kotlin 依赖项和应用程序 Gradle 版本。以下是我的 Android Studio 的 Gradle 文件详情:

buildscript {
 ext.kotlin_version = '1.3.10'    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

在这里,我们的 Gradle 版本是 3.2.1,Kotlin 版本是 1.3.10

Gradle 依赖项

在这个 Gradle 文件中,我们将实现 Retrofit 的依赖项,这将帮助我们从前一个项目中获取 JSON 类型的 REST API。以下是所有依赖项:

implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'

implementation 'com.google.code.gson:gson:2.8.5'

implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'com.squareup.retrofit2:retrofit-converters:2.5.0'
implementation 'com.squareup.retrofit2:retrofit-adapters:2.5.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.12.0'

创建用户模型

我们将获取基于基本身份验证的 Spring 项目的 REST API,该项目是使用基本身份验证创建的。尽管 REST API 有四个实体(idnameemailcontactNumber),但我们将基于这个 REST API 创建一个模型。

这是 REST API 的输出,我们可以看到五个用户的详细信息:

根据 API,我们将为客户端创建一个用户模型。这是名为 UserModel 的模型类:**

class UserModel (val id: String
                 val name: String,
                 val contactNumber: String,
                 val id: String,
                 val email: String)

现在,我们需要创建一个接口,它将包含 HTTP 请求函数。在这个项目中,我们只会使用一个 GET 函数来检索所有用户的详细信息。在这里,我们使用 GET Retrofit 注解来编码有关参数和请求函数的详细信息。

这是 UserService 接口的代码:

interface UserService {
 @GET("/user")
    fun getUserList(): Call<List<UserModel>>
}

我们将搜索 /user 端点,这将提供一个用户模型列表。

实现用户服务

Retrofit 客户端调用 Gerrit API 并通过将调用结果打印到控制台来处理结果。

创建一个类,我们将构建 Retrofit 客户端,这将调用 API 并处理结果。这将负责使用 Retrofit.builder 类控制所有任务,并使用给定 URL 的基础进行配置。

这是 UserServiceImpl.kt 的代码:

class UserServiceImpl{
   fun getGithubServiceImpl(username:String, password:String): UserService {
        val retrofit = Retrofit.Builder()
                .client(getOkhttpClient(username, password))
                .baseUrl(YOUR_SERVER_DOMAIN)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
        return retrofit.create(UserService::class.java)
    }

    private fun getOkhttpClient(username:String, password:String): OkHttpClient{
        return OkHttpClient.Builder()
                .addInterceptor(BasicAuthInterceptor(username, password))
                .build()
    }
}

根据这段代码,我们使用 usernamepassword 设置了 .client()。然后我们实现了 YOUR_SERVER_DOMAIN(假设 Rest API 服务器的 URL 为 http://localhost:8080),baseUrl(),并且我们使用了 OkHttpClient 作为客户端。

使用 OkHttp 拦截器进行身份验证

虽然我们使用的是基于基本身份验证的安全机制,但我们需要一个 usernamepassword 来授权访问这个 REST API。在这里,我们使用 OkHttp 拦截器进行身份验证。这将帮助您发送请求并获得访问资源的认证权限。

在这里,我们在 OkHttpClient.Builder() 中调用了 BasicAuthInterceptor 类:

 private fun getOkhttpClient(username:String, password:String): OkHttpClient{
        return OkHttpClient.Builder()
                .addInterceptor(BasicAuthInterceptor(username, password))
                .build()
    }

这是 BasicAuthInterceptor.kt 的类:

class BasicAuthInterceptor(user: String, password: String) : Interceptor {

    private val credentials: String = Credentials.basic(user, password)

    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val authenticatedRequest = request.newBuilder()
                .header("Authorization", credentials).build()
        return chain.proceed(authenticatedRequest)
    }
}

在这个类中,只添加了凭证作为用户详情。在这里,客户端将使用usernamepassword凭证发出请求。在每次请求期间,这个拦截器在执行之前起作用并修改请求头。因此,你不需要在 API 函数中添加@HEADER("Authorization")

调用回调

在这里,我们从MainActivity调用CallBack<>。这个回调响应来自服务器或离线请求。这意味着在稍后的时间点返回长时间运行函数的结果。

检查MainActivity.kt代码以使用CallBack函数并处理结果:

class MainActivity : AppCompatActivity() {

    var username: String = "sunnat629"
    var password: String = "password"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val githubService: UserService = UserServiceImpl().getGithubServiceImpl(username,password)

        val call: Call<List<UserModel>> = githubService.getUserList()
        call.enqueue(object: Callback<List<UserModel>> {
            override fun onFailure(call: Call<List<UserModel>>, t: Throwable) {
                Log.wtf("PACKTPUB", t.message)
            }

            override fun onResponse(call: Call<List<UserModel>>, response: Response<List<UserModel>>) {
                val adapter = UserListAdapter(this@MainActivity, response.body())
                displayList.adapter = adapter
            }
        })
    }
}

让我们如下讨论前面的代码:

  1. 首先,我们需要初始化UserServiceImpl().getGithubServiceImpl(username,password),这样我们就可以从UserService调用getUserList()

  2. 然后我们将调用enqueue(retrofit2.Callback<T>),这将异步执行,发送请求并获取响应。

  3. enqueue()有两个功能:onResponse()onFailure()。如果有任何与服务器相关的错误,它将调用onFailure(),如果它收到响应和资源,它将调用onResponse()。我们还可以使用onResponse()函数的资源。

在这里,我们将获取UserModel列表的响应。我们可以在应用程序 UI 中显示这个列表。

创建 UI

在创建的main_activity布局中,我们将显示用户详情的列表,其中显示用户的姓名、电子邮件 ID 和联系电话——我们将使用ListView

这是MainActivity类的mainActivity布局的代码:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/user_title"
        app:layout_constraintEnd_toEndOf="parent"
        android:textStyle="bold"
        android:padding="5dp"
        android:gravity="center_horizontal"
        android:textAppearance="?android:textAppearanceLarge"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ListView
        android:id="@+id/displayList"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

</android.support.constraint.ConstraintLayout>

在这个布局中,我们有一个TextView和一个ListView

我们将在MainActivityonResponse()函数中使用这个ListView

我们将获取列表并创建一个自定义适配器来显示用户列表,如下所示:

val adapter = UserListAdapter(this@MainActivity, 
response.body()//this is a arraylist 
)

在这里,我们有一个自定义适配器,我们将发送上下文和用户的Array列表。

创建自定义列表适配器

为了显示 REST API 的输出,我们需要创建一个自定义列表适配器,因此我们需要设计一个自定义列表适配器的 XML 文件。以下是列表中每一行的 XML 代码:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="10dp">

    <TextView
        android:id="@+id/name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:padding="5dp"
        android:textAppearance="?android:textAppearanceMedium"
        android:textStyle="bold"
        app:layout_constraintBottom_toTopOf="@+id/contactNumber"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="@tools:sample/full_names" />

    <TextView
        android:id="@+id/contactNumber"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:padding="5dp"
        android:textAppearance="?android:textAppearanceSmall"
        app:layout_constraintBottom_toTopOf="@+id/email"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/name"
        tools:text="@tools:sample/cities" />

    <TextView
        android:id="@+id/email"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:padding="5dp"
        android:textAppearance="?android:textAppearanceSmall"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/contactNumber"
        tools:text="@tools:sample/cities" />

</android.support.constraint.ConstraintLayout>

在这里,我们有一个包含namecontactNumberemailTextView

然后,我们将创建适配器,命名为UserListAdapter.kt,如下所示:

class UserListAdapter(context: Context,
                      private val userList: List<UserModel>?) : BaseAdapter() {
    private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)
            as LayoutInflater
    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        val rowView = inflater.inflate(R.layout.user_list_item, parent, false)
        val name = rowView.findViewById(R.id.name) as TextView
        val email = rowView.findViewById(R.id.email) as TextView
        val contactNumber = rowView.findViewById(R.id.contactNumber) as TextView
        val userDetails = getItem(position) as UserModel
        name.text = userDetails.name
        email.text = userDetails.email
        contactNumber.text = userDetails.contactNumber
        return rowView
    }
    override fun getItem(position: Int): Any {
        return userList!![position]
    }
    override fun getItemId(position: Int): Long {
        return position.toLong()
    }
    override fun getCount(): Int {
        return userList!!.size
    }
}

这个类扩展了BaseAdapter(),这将添加几个继承的功能。

然后你需要添加LayoutInflater,它将 XML 布局转换为相应的ViewGroupsWidgets

  • getView()为列表的一行创建一个视图。在这里,你需要定义所有基于 UI 的信息。

  • getItem()返回从服务器获取的列表位置。

  • getItemId()为列表中的每一行定义一个唯一的 ID。

  • getCount()返回列表的大小。

现在,在getView()中,你将添加布局元素,如下所示:

 val name = rowView.findViewById(R.id.name) as TextView
        val email = rowView.findViewById(R.id.email) as TextView
        val contactNumber = rowView.findViewById(R.id.contactNumber) as TextView

你永远不应该在主线程上执行长时间运行的任务。这将导致应用程序无响应(ANR)。

移动应用程序

一旦我们完成了代码,就是时候查看输出了。运行你的基本认证 Spring 项目,然后运行你的应用程序。以下是应用程序的输出,我们可以看到用户详情:

图片

在下面的屏幕截图中,左侧是服务器 API,其中包含用户详情,右侧是 Android 应用程序的客户端输出:

图片

我们创建了一个客户端应用程序,它将获取基于基本认证的 Spring-Security REST API 的数据。

使用 Spring Security OAuth2 保护 REST

在最后一节,我们学习了如何创建一个基本的授权项目。这为项目提供了坚实的基础安全,但它不具备复杂或企业级项目所需的安全维度。由于这种安全可能被破解或黑客攻击,我们需要一个更稳固的安全框架来处理这类黑客攻击。OAuth 是最好的安全框架之一——它被 Google、Facebook、Twitter 和许多其他流行的平台广泛使用。现在我们将学习 OAuth2 及其应用。

什么是 OAuth2?

OAuth 是一种安全的授权协议,OAuth2 是 OAuth 协议的第二版。这个协议被称为框架。OAuth2 允许第三方应用程序提供对 HTTP 服务的有限访问,例如 Google、GitHub 或 Twitter。这种访问要么是为了所有者的利益,要么是为了使第三方应用程序能够访问用户账户。这就在网页和桌面或移动设备之间创建了一个授权流。它有一些重要的角色,用于控制用户的访问限制。

OAuth2 角色

OAuth2 有四个角色:

  • 资源所有者: 通常情况下,这就是你。

  • 资源服务器: 服务器托管受保护的数据。例如,Google、Github 或 Twitter 托管你的个人和职业信息。

  • 客户端: 一个请求资源服务器访问数据的程序。客户端可以是网站、桌面应用程序,甚至是移动应用程序。

  • 授权服务器: 这个服务器将向客户端颁发访问令牌。这个令牌将是访问信息的密钥,它主要用于请求资源服务器以供客户端使用。

这是 OAuth 协议的一般工作流程图(每个协议的流程并不固定;它基于授权的类型):

图片

下面是工作流程的步骤:

  1. 为了访问服务资源,应用程序用户发送授权请求

  2. 如果用户授权请求,应用程序将收到授权许可

  3. 应用程序将授权许可发送给授权服务器以获取访问令牌

  4. 如果授权许可有效且应用程序已认证,授权服务器将创建一个访问令牌

  5. 应用程序授权服务器 获取 访问令牌

  6. 应用程序向 资源服务器 发送请求,以从服务器获取资源以及进行身份验证。

  7. 使用令牌,资源服务器应用程序 提供请求的资源。

OAuth2 授权类型

有四种类型的 OAuth2 授权:

  • 授权代码:在服务器端应用程序中使用,允许客户端获取一个长期访问令牌。然而,如果客户端请求服务器获取新令牌,此令牌将被无效化。

  • 隐式:大部分情况下,这用于移动或 Web 应用程序。

  • 资源所有者密码凭证:在这个授权中,凭证首先发送给客户。然后它们被发送到授权服务器。

  • 客户端凭证:当客户端本身是资源所有者时使用。不需要从客户端端获取授权。

因此,这是 OAuth 协议的简要总结。现在让我们使用 Spring Security OAuth2 模块创建一个项目。

创建项目

我们将创建一个简单的基于 Spring Security OAuth2 的项目。为此,请访问 start.spring.io/ 并根据您的需求修改给定的字段。

在这里,我们使用 Maven 项目,并将语言选择为 Kotlin。Spring Boot 版本为 2.1.1 (SNAPSHOT)。

选择生成项目后,您将获得一个 ZIP 文件的项目。解压并使用您的 IDE 打开此项目。

Maven 依赖项

我们的主要依赖项是 WebSecurityCloud SecurityCloud OAuth2JPAH2LombokThymeleaf

下面是 pom.xml 中提到的 Maven 依赖项:

----
----
  <dependencies>
---
---
<!--spring security-->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>5.2.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!--spring cloud security-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>

----
----

<!--database-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
----
----

配置资源服务器

资源服务器将拥有所有受保护的资源,这些资源由 OAuth2 令牌保护。现在是时候借助代码来了解这个资源服务器了。创建一个名为 ResourceServerConfig.kt 的资源服务器。

下面是我们的 ResourceServerConfig.kt 代码:

@Configuration
@EnableResourceServer
class ResourceServerConfig: ResourceServerConfigurerAdapter(){

    @Throws(Exception::class)
    override fun configure(http: HttpSecurity?) {
        http!!
                .authorizeRequests()
                .antMatchers("/open_for_all").permitAll() // anyone can enter
                .antMatchers("/private").authenticated() // only authorized user can enter
    }
}

要启用 OAuth 2.0 资源服务器机制的功能,您需要添加一个名为 @EnableResourceServer 的注解,尽管它是一个配置类,但您需要添加 @Configuration 注解。

此类扩展 ResourceServerConfigurerAdapter,然后扩展 ResourceServerConfigurer,这将使其能够覆盖和配置 ResourceServerConfigurer

我们覆盖 configure(http: HttpSecurity?),其中我们提到哪些 URL 路径受保护,哪些不受保护。

authorizeRequests() 允许根据 HttpServletRequest 的使用来限制访问。

antMatchers() 指的是映射中 Ant 风格路径模式实现的实现。

我们使用 .antMatchers("/").permitAll(),这允许所有用户访问此 URL 路径 "/"。此外,我们使用 .antMatchers("/private").authenticated(),这意味着用户需要令牌才能访问此 /private 路径。

配置授权服务器

授权服务器是一个配置类。在这个类中,我们将创建一个授权类型环境。授权类型帮助客户端从最终用户那里获取访问令牌。这个服务器的配置旨在实现客户端详情服务和令牌服务。它还负责全局启用或禁用机制中的某些组件。现在,创建一个名为 AuthorizationServerConfig.kt 的授权服务器类。

这是 AuthorizationServerConfig.kt 的代码:

@Configuration @EnableAuthorizationServer
class AuthorizationServerConfig: AuthorizationServerConfigurerAdapter() {

   @Autowired
   lateinit var authenticationManager: AuthenticationManager

    @Autowired
    lateinit var passwordEncoder: BCryptPasswordEncoder

    @Throws(Exception::class)
    override fun configure(security: AuthorizationServerSecurityConfigurer?) {
        security!!.checkTokenAccess("isAuthenticated()")
    }

    @Throws(Exception::class)
    override fun configure(clients: ClientDetailsServiceConfigurer?) {
       clients!!
               .inMemory()
               .withClient("client")
               .secret(passwordEncoder.encode("secret"))
               .authorizedGrantTypes("password")
               .authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
               .scopes("read", "write", "trust")
               .resourceIds("oauth2-resource")
               .accessTokenValiditySeconds(5000) // token validity time duration 5 minuets

    }

    @Throws(Exception::class)
    override fun configure(endpoints: AuthorizationServerEndpointsConfigurer?) {
        endpoints!!.authenticationManager(authenticationManager)
    }
}

@EnableAuthorizationServer 注解启用了 OAuth 2.0 授权服务器机制的功能。您需要添加 @Configuration 注解以使其成为配置类。

这个类扩展了 AuthorizationServerConfigurerAdapter,它又扩展了 ResourceServerConfigurer。这将使得能够覆盖和配置 AuthorizationServerConfigurer。有三个类型的 configure() 函数:

  • ClientDetailsServiceConfigurer: 这定义了客户端的详情服务。

  • AuthorizationServerSecurityConfigurer: 这定义了令牌端点的安全约束。

  • AuthorizationServerEndpointsConfigurer: 这定义了授权和令牌端点以及令牌服务。

根据我们的代码,在 configure(security: AuthorizationServerSecurityConfigurer?) 中,我们定义了是否检查已认证的令牌端点。

configure(clients: ClientDetailsServiceConfigurer?) 中,我们定义了 ClientDetails 服务。在这个项目中,我们没有使用数据库,因此我们使用 ClientDetails 服务的内存实现。以下是客户端的重要属性:

  • withClient(): 这是必需的,这是定义客户端 ID "client" 的地方。

  • secret(): 这是受信任客户端必需的,这是定义密钥 "secret" 的地方,但我们必须对密码进行编码。在这里,我们注入 BCryptPasswordEncoder 来编码密码或密钥。

  • authorizedGrantTypes(): 我们使用了 "password" 授权类型,这是客户端被授权使用的。

  • scope(): 范围用于限制客户端对资源的访问。如果范围未定义或为空,则表示客户端不受范围限制。在这里,我们使用 "read""write""trust"

  • authorities(): 这用于授予客户端。

  • resourceId(): 这是一个可选 ID,用于资源。

  • accessTokenValiditySeconds(): 这指的是令牌的有效时间长度。

configure(endpoints: AuthorizationServerEndpointsConfigurer?) 中,我们已配置了 AuthorizationEndpoint,它支持授权类型。我们注入 AuthenticationManager 并通过 AuthorizationServerEndpointsConfigurer 进行配置。

创建安全配置

这是一个用于 Spring Security 的 Java 配置类,它允许用户在不使用 XML 的情况下轻松配置 Spring Security。创建一个名为 SecurityConfiguration.kt 的安全配置文件。以下是类的代码:

@Configuration
@EnableWebSecurity
class SecurityConfiguration: WebSecurityConfigurerAdapter() {

    @Throws(Exception::class)
    override fun configure(auth: AuthenticationManagerBuilder?) {
        auth!!
                .inMemoryAuthentication()
                .passwordEncoder(passwordEncoder())
             // user1 as USER
                .withUser("sunnat")
                .password(passwordEncoder().encode("password"))
                .roles("USER")
                .and()

                // user2 as ADMIN
               .withUser("admin")        
               .password(passwordEncoder().encode("password"))
                .roles("ADMIN")
    }

    @Throws(Exception::class)
    override fun configure(http: HttpSecurity?) {
        http!!
                .antMatcher("/**").authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .httpBasic()
    }

    @Bean(name = [BeanIds.AUTHENTICATION_MANAGER])
    @Throws(Exception::class)
    override fun authenticationManagerBean(): AuthenticationManager {
        return super.authenticationManagerBean()
    }

    @Bean
    fun passwordEncoder(): BCryptPasswordEncoder {
        return BCryptPasswordEncoder(16)
    }
}

这是一个配置类,因此你需要添加 @Configuration 注解。

此类扩展了 WebSecurityConfigurerAdapter,而 @EnableWebSecurity 注解提供了基于 Web 的安全机制。

根据此代码,我们在必需的功能中使用了两个 @Bean 注解。我们注入 AuthenticationManager 并通过 AuthorizationServerEndpointsConfigurer 进行配置。使用 BCryptPasswordEncoder 实例来编码密码。

configure(http: HttpSecurity?) 中,请注意以下内容:

  • antMatcher("/**").authorizeRequests() 表示此 HttpSecurity 只适用于以 /** 开头的 URL。

  • anyRequest().authenticated() 的使用保证了任何对我们的应用程序的请求都需要客户端进行确认。

  • formLogin() 允许用户通过基于表单的登录进行身份验证。

  • httpBasic() 表示用户通过 HTTP Basic 身份验证进行验证。

configure(auth: AuthenticationManagerBuilder?) 中,请注意以下内容:

  • inMemoryAuthentication() 包括将内存确认添加到 AuthenticationManagerBuilder,并将 InMemoryUserDetailsManagerConfigurer 恢复以允许自定义内存验证。

  • passwordEncoder(passwordEncoder()) 表示密码将是一个编码密码。

  • withUser("user")withUser("admin") 是用户的名称。

  • password(passwordEncoder().encode("password")) 是编码后的密码。

  • roles("USER")roles("ADMIN") 是用户的角色。

创建控制器类

创建一个名为 UserController.kt 的控制器类,如下所示:

@RestController
@RequestMapping("/")
class UserController{

//    This is for all means there is no security issue for this URL path
    @GetMapping(value = ["/open_for_all", ""])
    fun home(): String{
        return "This area can be accessed by all."
    }

    //    Yu have to use token to get this URL path
    @GetMapping("/private")
    fun securedArea(): String{
        return "You used an access token to enter this area."
    }
}

在这里,我们将此类标注为 @RestController,它处理所有 Web 请求。@RequestMapping("/") 表示默认 URL 路径是 "/"

@GetMapping 实现的功能是 home(),任何人都可以访问,以及 securedArea(),只有拥有 访问令牌 的人才能访问。我们在 ResourceServerConfig 类中配置了这些。

创建应用程序类

最后,创建一个名为 SpringSecurityOAuth2Application.kt 的应用程序类,这将把你的应用程序转换为 SpringBoot 应用程序:

@SpringBootApplication
class SpringSecurityOAuth2Application

fun main(args: Array<String>) {
    runApplication<SpringSecurityOAuth2Application>(*args)
}

应用程序属性

此步骤是可选的,尤其是在这个项目中。在这里,我们只是更改了这个项目的端口号。要更改它,请修改 resources 文件夹下的 application.properties

#this project server port
server.port=8081

在这里,我们将端口号更改为 8081

检查输出

如果你正在阅读本节,这意味着你已经正确配置了一切。完成项目后,你将拥有以下文件:

完成设置后,运行项目。如果没有错误,你可以找到运行窗口。以下截图显示没有错误,应用程序已准备好使用:

检查未受保护的 URL

现在,打开 Insomnia 应用程序。

从顶部邮箱创建一个 GET 请求,并使用 http://localhost:8081/open_for_all 的 URL。

你的结果将类似于以下截图:

图片

ResourceServerConfig类中,我们配置了"/open_for_all"可以被每个人访问。

获取访问令牌

从顶部邮箱创建一个POST请求,并写下http://localhost:8081/oauth/token URL。这是获取令牌的默认POST URL

在多部分窗口中添加三个参数—username=sunnatpassword=passwordgrant_type=password—:

图片

你可以在SecurityConfiguration类中找到usernamepassword的信息,而grant_type可以在AuthorizationServerConfig中找到。在这里,我们使用密码授权类型。

前往基本窗口并输入用户名密码。你可以在AuthorizationServerConfig类中找到这些信息,其中用户名在withClient()中提及,密码在secret()中。

我们添加了一张图片,展示了我们记录了用户名密码的 Insomnia 工具。现在点击发送按钮。如果没有错误,你将获得以下access_token

图片

你可以看到将用于访问受保护资源的access_token。"expires_in"表示在4469秒后令牌将过期。"scope": "read write trust"表示你可以读取、写入和修改资源。

访问受保护的 URL

我们找到了access_token,现在我们将使用它。为此,创建另一个GET请求并插入http://localhost:8081/private

作为参数,使用具有给定令牌键值的access_token,然后点击发送:

图片

完成这些操作后,你可以访问受保护的/private URL,该 URL 在ResourceServerConfig类中进行了配置。

我们现在已准备好在我们的项目中使用 OAuth2 Spring Security。

常见错误和错误

在这个项目中,你可能会遇到一些常见错误。

例如,在构建和运行项目时可能会遇到一些错误。为了解决这个问题,请检查所有依赖项的版本是否为最新。此外,请确保每个依赖项都已存在。如果你使用数据库,请确保在application.properties中你有正确的数据库和模式名称。

POST请求中,有时你会找到以下错误信息:

图片

之前的截图表明你输入了错误的grant_type。请检查参数以及你提及grant_typeAuthorizationServerConfig类:

图片

请检查SecurityConfiguration类,并将系统的username-password与提供的usernamepassword参数匹配。以下截图表示你在Basic Auth选项卡中输入了错误的客户端或密钥值:

图片

上述截图表示你在基本认证选项卡中输入了错误的 clientsecret 值。请将 AuthorizationServerConfig 中的 clientsecret 值与基本认证选项卡中的值进行匹配:

上述截图表示您的令牌密钥已过期。您需要刷新一个新的访问令牌来解决此错误。

你可能会遇到其他错误。要查看解决方案,你总是可以搜索 StackOverflow (stackoverflow.com/)。

摘要

在本章中,你学会了如何自信地使用 Spring Security。首先,我们介绍了 Spring Security 是什么以及其架构。我们还了解了使用 Spring Security 的优势,通过其特性和模块进行了探讨。现在,我们能够在任何项目中实现 Spring Security。我们学习了基本认证是什么,并通过一个示例展示了如何在项目中实现基本认证以及如何保护服务器中的资源。我们还学习了如何创建一个安全的 REST API。然后我们学习了如何创建一个 Android 客户端应用程序来从 REST API 中获取和使用受保护的资源。我们还学习了如何实现用户名和密码以获取基于基本认证的安全服务器的访问权限。此外,我们还熟悉了如何在客户端应用程序中的 listview 中使用自定义适配器。在最后一节中,我们探索了一个更安全的协议:OAuth2。我们学习了该协议的角色和工作流程。通过一个简单的项目,我们学习了如何配置 OAuth2 授权和资源服务器。最后,我们看到了如何使用第三方 HTTP 客户端检索 REST API。

在下一章中,我们将学习数据库,它非常重要,因为它是存储和处理您数据的主要地方。

问题

  1. Spring Security 的目标是?

  2. Spring Security 的基本类有哪些?

  3. 需要哪个过滤器类用于 Spring Security?

  4. Spring Security 是否支持密码散列?

  5. OAuth 2.0 授权类型有哪些?

进一步阅读

这里有一份您可以参考的信息列表:

第六章:访问数据库

在本章中,我们将学习 Spring 框架中的数据库。数据库是一个以有组织的方式存储在服务器上的数据集合,以便应用程序可以以用户请求的方式检索数据。在本章中,您将学习如何在客户端和服务器端使用数据库。此外,我们将从服务器端探索 JDBC、JPA、Hibernate 和 MySQL 的使用,并从客户端端查看 room 持久库。

本章涵盖以下主题:

  • 什么是数据库?

  • 什么是数据库管理系统?

  • Spring 中的数据访问。

  • Spring 中的 JDBC 数据访问。

  • 使用 JDBC 创建示例项目。

  • 在 Spring 中使用 JPA 和 Hibernate 进行数据访问。

  • 使用 JPA + Hibernate 创建示例项目。

  • 什么是 room 持久库?

  • 使用 room 持久库创建 Android 应用程序。

技术要求

我们之前已经演示了如何设置环境以及开发 Spring 所需的工具、软件和 IDE。要创建您的项目,请访问此链接:start.spring.io/。这里将提供以下选项:

  • Maven 项目

  • 语言 – Kotlin

  • Spring Boot 版本 – 2.1.1 (SNAPSHOT)

  • 当您创建项目时,您需要提供一些信息,例如——组别工件名称描述包名打包方式Java 版本

我们将在即将到来的项目中使用 MySQL。因此,您需要从 dev.mysql.com/downloads/workbench/ 下载 MySQL 工具并安装它。请尝试使用给定信息配置 MySQL 数据库,以便使您的项目更容易:

Host -- localhost
Port -- 3306
Username -- root
Password -- 12345678

本章的示例源代码可在 GitHub 上的以下链接找到:github.com/PacktPublishing/Learn-Spring-for-Android-Application-Development/tree/master/Chapter06

数据库

数据库是一个以有组织的方式存储在服务器上的信息集合。用户可以从服务器在各种系统中检索和使用这些数据。在数据库中,用户可以添加、删除、更新、获取或管理数据。通常,数据被组装成表格、列和行,这使得查找相关数据变得更容易。计算机数据库包含数据记录或文件的聚合。公司的数据可以包括他们的统计数据或客户信息,或者可能是绝密文件。数据库管理员为客户提供或用户控制读写访问、分析数据等的能力。现在我们将探讨各种数据库类型及其用途。

数据库类型

数据库用于各种目的,例如存储个人或公司信息。市场上有几种数据库,如下文所述。

个人数据库

个人数据库是为存储在个人计算机上的数据设计的。这个数据库很小,非常易于管理,通常由一小群人或一个小组织使用。

关系型数据库

关系型数据库是在一组适合预定义类别的表中创建的。这些数据库通过表格的排列进行排序,其中信息适合预定义的类别。表由行和列组成。列有一个信息传递的通道,用于明确的分类。另一方面,行包含一个信息案例,该信息由分类所表征。关系型数据库有一个名为结构化查询语言SQL)的标准用户和应用程序程序接口。

分布式数据库

分布式数据库存储在多个物理位置,并在组织的各个地点进行分布。这些地点通过通信链路连接,因此用户可以轻松访问分布式数据。分布式数据库有两种类型——同构和异构。在同构分布式数据库中,物理位置具有相同的硬件,运行在相同的操作系统和数据库应用程序中。然而,在异构分布式数据库中,硬件、操作系统或数据库应用程序可能位于不同的位置。

面向对象数据库

在面向对象的数据库中,项目是通过使用面向对象编程(如 Java 和 C++)创建的,这些项目存储在关系型数据库中。但对于这些项目,面向对象的数据库非常适合。面向对象的数据库是围绕对象而不是活动,以及信息而不是理由来排序的。

NoSQL 数据库

NoSQL 数据库通常用于大量分布式数据。这种数据库在处理大数据方面非常有效,其中组织分析存储在云中多个虚拟服务器上的大量未组织数据。

图数据库

图数据库是一种使用图论来存储、映射和查询数据关系的 NoSQL 数据库。它是由许多节点和边组成的集合。节点代表实体,边代表节点之间的连接。这种数据库在社交媒体平台如 Facebook 上使用得很多。

云数据库

云数据库主要是为虚拟化环境构建的。虚拟化环境可以是混合云、公有云或私有云。这些数据库提供了各种好处,例如按存储容量和按用户基础带宽付费的能力。作为一个软件即服务(SaaS),它为企业的业务应用提供支持。

数据库管理系统

数据库管理系统(DBMS)是一种系统软件,用于创建和管理数据库。借助 DBMS,用户或开发者可以以系统化的方式创建、获取、更新和管理数据。这个系统在用户和数据库之间充当一种接口。它还确保数据得到一致的组织并易于访问。

下面是关于使用 DBMS 的图示:

数据库管理系统(DBMS)有三个重要特性,这些特性包括数据、数据库引擎和数据库模式。数据是一系列信息的集合,数据库引擎允许数据被锁定、访问和修改,而数据库模式定义了数据库的逻辑结构。

DBMS 提供了一个通用的视图,说明多个用户可以从多个位置以受控的方式访问数据。它还限制了用户对用户数据的访问。数据库模式提供了用户如何查看数据的逻辑。DBMS 处理所有请求并在数据库上执行它们。

DBMS 提供了逻辑和物理数据独立性。这意味着应用程序可以使用 API 来利用数据库中的数据。此外,客户端和应用程序无需担心存储数据的地点以及数据物理结构的更改,如存储和硬件。

流行数据库模型及其管理系统包括以下内容:

  • 关系数据库管理系统(RDBMS

  • NoSQL DBMS

  • 内存数据库管理系统(IMDBMS

  • 列式数据库管理系统(CDBMS

  • 基于云的数据管理系统

Spring 中的数据访问

数据访问负责授权访问数据存储库。它有助于区分 角色 能力,如应用程序中的用户或管理员。它维护数据访问系统,如基于角色的插入、检索、更新或删除。在 第三章,Spring 框架概述 中,我们学习了 Spring 的架构。

下面是 Spring 架构的图示,其中 数据访问 是其中一层:

如您所见,数据访问 是 Spring 架构的层之一。这部分关注数据访问。JDBCORMOXMJMS事务 模块是 Spring 中使用的模块。我们已在 第三章,Spring 框架概述 下的 Spring 架构主题中提到了这些细节。在本章中,我们将看到 JDBCORMJPAHibernate)的使用。

Spring 中的 Java 数据库连接

Java 数据库连接JDBC)是一个连接和从前端到后端移动数据的 API 规范。类和接口是用 Java 编写的。如今,它也支持 Kotlin。我们将在本章中用 Kotlin 编写。这基本上充当了基于 Java 的应用程序和数据库之间的接口或桥梁。JDBC 与 开放数据库连接ODBC)非常相似。像 ODBC 一样,JDBC 允许 JDBC 应用程序访问数据集合。

在 Spring 框架中,JDBC 被分为以下四个独立的包:

  • 核心:这是 JDBC 的核心功能,JdbcTemplateSimpleJdbcInsertSimpleJdbcCall 是这个核心部分的重要类

  • 数据源:用于访问数据源

  • 对象:JDBC 可以以面向对象的方式访问。作为一个业务对象,它执行查询并返回结果

  • 支持:支持类在核心和对象包下工作

使用 JDBC 创建示例项目

让我们通过一个项目来学习 JDBC,在这个项目中,我们将为用户创建 REST API 并显示用户详情列表。在这个项目中,我们将使用 JDBC、MySQL 和 Spring Boot。

要创建一个项目,请访问此链接:start.spring.io 并创建一个基于 Kotlin 的项目。以下是 JDBC 的依赖项:

  • JDBC:这将实现与 JDBC 相关的所有功能

  • MySQL:这将实现 MySQL 数据库的所有功能

Maven 依赖项

如果您转到 pom.xml 文件,您将看到 JDBC 的依赖项,我们使用 MySQL 作为数据源。以下是 pom.xml 文件的代码片段:

-----
-----
<!-- This is for JDBC use -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
-----
-----

<!-- This is for use the MySQL -->
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <scope>runtime</scope>
</dependency>
-----
-----

创建数据源

我们在 application.properties 中配置了 DataSource 和连接池。Spring Boot 使用 spring.datasource 接口作为前缀来配置 DataSource。我们的数据库模式名称是 packtpub_dbtest_schema。您可以自己创建它并重命名。以下是 application.properties 的详情:

# Database Configuration

spring.datasource.url=jdbc:mysql://localhost:3306/packtpub_dbtest_schema
spring.datasource.username=root
spring.datasource.password=12345678

根据前面的代码,spring.datasource.url=jdbc:mysql://localhost:3306/packtpub_dbtest_schema 表示访问项目中的数据时,名为 packtpub_dbtest_schema 的数据库模式的 URL。spring.datasource.username=root 表示数据库的用户名为 root,而 spring.datasource.password=12345678 表示数据库的密码为 12345678

在我们的系统中,MySQL 的详情如下:

Host -- localhost                                    // the host URL
Port -- 3306                                         // the host POST number
Username -- root                                     // the username of the database
Password -- 12345678                                 // the password of the database
Database Name - packtpub_dbtest                      // the Database name
Database Schema Name - packtpub_dbtest_schema        // the Database Schema name

在数据库中创建表

前往 MySQL Workbench 并选择数据库。

我们为 USERS 表包含了一些用户详情。您可以将以下代码复制并粘贴以创建一个 USERS 表并插入一些示例数据:

create table users (id int not null auto_increment, name varchar(255), email varchar(255), contact_number varchar(255)
, primary key (id)) engine=MyISAM;
INSERT INTO user (id, name, email, contact_number) values (1, 'Sunnat', 'sunnat629@gmail.com', '1234567890');
INSERT INTO user (id, name, email, contact_number) values (2, 'Chaity', 'chaity123@gmail.com', '9876543210');
INSERT INTO user (id, name, email, contact_number) values (3, 'Mirza', 'mirza123@gmail.com', '1234567800');
INSERT INTO user (id, name, email, contact_number) values (4, 'Hasib', 'hasib123@gmail.com', '1234500800');
INSERT INTO user (id, name, email, contact_number) values (4, 'Jisan', 'jisan123@gmail.com', '1004500800');

在用户表中插入用户详情后,您可以在您的 users 表中看到内容,如下面的截图所示:

创建模型

在这个项目中,我们将创建一个 REST API 来查看用户详情列表,我们可以获取用户名、电子邮件 ID 和联系电话。因此,让我们创建一个用户模型;类的名称是 UserModel.kt

下面是模型类的代码:

data class UserModel(val id: Int,
                     val name: String,
                     val email: String,
                     val contact_number: String)

我们已经创建了一个名为 UserModel 的类,其中我们初始化了 idnameemailcontact_number

创建行映射器

RowMapper 是由 Spring JDBC 提供的一个接口。这个接口用于将一行数据映射到一个 Java 对象,并从数据库中获取数据。它使用 JdbcTemplate 类的 query() 函数。让我们创建一个名为 UserRowMapper.ktRowMapper 接口。

下面是这个接口的代码:

class UserRowMapper : RowMapper<UserModel> {

    @Throws(SQLException::class)
    override fun mapRow(row: ResultSet, rowNumber: Int): UserModel? {
        return UserModel(row.getInt("id"),
                row.getString("name"),
                row.getString("email"),
                row.getString("contact_number"))
    }
}

在这段代码中,我们扩展了 RowMapper<UserModel> 并覆盖了 mapRow 方法,其中我们返回 UserModel

创建 API 接口

为了获取 REST API 的响应,我们需要创建一个接口,在其中我们将说明我们想要对数据进行什么操作,例如获取用户列表、创建新用户或删除或更新用户详情。让我们创建一个名为 UserInterface.kt 的接口。

下面是这个接口的代码:

interface UserInterface {
    fun getAllUserList(): List<UserModel>
    fun getUserByID(id: Int): UserModel?
    fun addNewUser(userModel: UserModel)
    fun updateUser(userModel: UserModel)
    fun deleteUser(id: Int)
}

我们使用了五个函数,下面将逐一解释:

  • getAllUserList(): 这将返回所有用户详情的列表

  • getUserByID(id: Int): 这将返回特定用户的详情

  • addNewUser(userModel: UserModel): 这将添加新的用户详情

  • updateUser(userModel: UserModel): 这将更新现有用户的详情

  • deleteUser(id: Int): 这将删除特定用户

创建用户仓库

我们将在这个类中与数据库进行通信。这是一个仓库类,因此我们使用 @Repository 注解来标记这个类。让我们创建一个名为 UserRepository.kt 的仓库类,它实现了 UsersInterface

下面是这个仓库类的代码:

@Repository
class UserRepository: UsersInterface {

    override fun getAllUserList(): List<UserModel> {
    }

    override fun getUserByID(id: Int): UserModel? {
    }

    override fun addNewUser(userModel: UserModel) {
    }

    override fun updateUser(userModel: UserModel) {
    }

    override fun deleteUser(id: Int) {
    }
}

我们已经创建了一个名为 UserRepository 的仓库类,其中我们实现了 UsersInterface 并覆盖了接口中的所有函数。我们使用 @Repository 注解使其成为一个仓库类。

我们将在下一节逐步完成这个类的创建。

JdbcTemplate 实现

JdbcTemplate 是 JDBC 的核心。这是 JDBC 的中心类。SQL 查询由 JdbcTemplate 执行,它也获取结果。要使用这个 JdbcTemplate,我们需要在这个仓库类中自动装配 JdbcTemplate。下面是这个仓库类的代码片段:

@Repository
class UserRepository: UserInterface {

    @Autowired
    private lateinit var jdbcTemplate: JdbcTemplate
    ----
    ----
  }

创建 RESTful API 的 HTTP 方法

对于这个项目,我们将创建 创建、读取、更新和删除CRUD) 操作。

创建

查找与创建操作相关的代码片段,其中我们将插入用户详情:

override fun addNewUser(userModel: UserModel) {
    val addQuery = "INSERT INTO users (name, email, contact_number) values (?,?,?)"
    jdbcTemplate.update(addQuery,userModel.name,userModel.email,userModel.contact_number)
}

addQuery = "INSERT INTO users (name, email, contact_number) values (?,?,?)" 是将用户插入 USER 表的查询。

jdbcTemplate.update() 是一个函数,我们使用查询和用户详情作为参数将其插入数据库。

读取

查找与读取操作相关的代码片段。以下函数将返回所有用户详情的列表:

override fun getAllUserList(): List<UserModel> {
    val selectAllSql = "SELECT * FROM users"
    return jdbcTemplate.query(selectAllSql, UserRowMapper())
}

selectAllSql = "SELECT * FROM users"是查询从用户表中获取所有用户的查询。jdbcTemplate.query()将执行查询并获取数据。

以下函数将根据id获取用户的详细信息:

override fun getUserByID(id: Int): UserModel? {
    val selectAllSql = "SELECT * FROM users WHERE id = ?"
    return jdbcTemplate.queryForObject(selectAllSql, UserRowMapper(), id)
}

selectAllSql = "SELECT * FROM users WHERE id = ?"是使用 ID 从用户表中获取用户的查询。jdbcTemplate.queryForObject()将执行查询并获取数据。

更新

查找更新操作的代码片段:

override fun updateUser(userModel: UserModel) {
    val updateQuery = "UPDATE users SET name=?,email=?, contact_number=? WHERE id=?"
    jdbcTemplate.update(updateQuery, userModel.name, userModel.email, userModel.contact_number, userModel.id)
}

updateQuery = "UPDATE users SET name=?,email=?, contact_number=? WHERE id=?"是使用 ID 从用户表中更新用户的查询。jdbcTemplate.update()将执行查询并更新数据。

删除

查找删除操作的代码片段:

override fun deleteUser(id: Int) {
    val deleteQuery = "DELETE FROM users WHERE id=?"
    jdbcTemplate.update(deleteQuery, id)
}

deleteQuery = "DELETE FROM users WHERE id=?"是使用 ID 从用户表中更新用户的查询。jdbcTemplate.update()将执行查询并删除特定数据。

通过这些函数,我们已经完成了我们的仓库类。

创建服务

在创建仓库类之后,让我们创建一个服务类,在这个类中我们将使用@autowired注解来自动装配仓库类。让我们创建一个名为UserService.kt的服务类,并使用@Service注解,它实现了UserInterface并覆盖了所有函数。

这是UserService.kt的代码片段**:

@Service
class UserService: UsersInterface {

    @Autowired
 private lateinit var userRepository: UserRepository

    ------
    ------
}

让我们借助UserRepository来覆盖和修改函数。以下是UserService类的完整代码:

@Service
class UserService: UsersInterface {
    @Autowired
    private lateinit var userRepository: UserRepository

    override fun getAllUserList(): List<UserModel> {
        return userRepository.getAllUserList()
    }

    override fun getUserByID(id: Int): UserModel? {
        return userRepository.getUserByID(id)
    }

    override fun addNewUser(userModel: UserModel) {
        userRepository.addNewUser(userModel)
    }

    override fun updateUser(userModel: UserModel, id: Int) {
        userRepository.updateUser(userModel, id)
    }

    override fun deleteUser(id: Int) {
        userRepository.deleteUser(id)
    }
}
  • getAllUserList(): 这个函数将获取所有用户

  • getUserByID(id: Int): 这个函数将根据 ID 获取用户

  • addNewUser(userModel: UserModel): 这个函数将插入一个新用户

  • updateUser(userModel: UserModel, id: Int): 这个函数将根据 ID 更新用户

  • deleteUser(id: Int): 这个函数将根据 ID 删除用户

创建控制器

如果你的模型、仓库和服务类都已完成,那么你就可以创建控制器类了,在这个类中我们将创建GetMappingPostMappingPutMappingDeleteMapping来创建 RESTful API URL 路径。让我们使用@RestController注解创建一个名为UserController.kt的控制器类:

@RestController
class UserController {
    ----
    ----
}

自动装配服务

让我们使用@Autowired注解来自动装配UserService。以下是UserController类的代码片段:

 @Autowired
 private lateinit var userService: UserService

获取用户列表

查找getAllUsers()操作的代码片段:

//    Getting the User List
@GetMapping(path = ["/users"])
fun getAllUsers(): ResponseEntity<List<UserModel>> {
    return ResponseEntity(userService.getAllUserList(),
            HttpStatus.OK)
}

@GetMapping(path = ["/users"])注解是/users的 URL 路径,它是一个GET请求函数。在这里,我们将从数据库中获取用户列表。

根据 ID 获取一个用户

查找getAllUserByID()操作的代码片段:

//    Getting one User by ID
@GetMapping(path = ["/user/{id}"])
fun getAllUserByID(@PathVariable("id") id: Int): ResponseEntity<UserModel> {
    return ResponseEntity(userService.getUserByID(id),
            HttpStatus.OK)
}

@GetMapping(path = ["/user/{id}"])注解是"/user/{id}"的 URL 路径,它是一个带有特定 ID 的GET请求。在这里,我们将从数据库中获取特定用户的详细信息。

插入新用户

查找addNewUser()操作的代码片段:

//    Inserting new User
@PostMapping(path = ["/user/new"])
fun addNewUser(@RequestBody userModel: UserModel): String {
    ResponseEntity(userService.addNewUser(userModel), HttpStatus.CREATED)
    return "${userModel.name} has been added to database"
}

@PostMapping(path = ["/user/new"])注解是"/user/new"的 URL 路径,它是一个POST请求。在这里,我们可以将用户详细信息插入到数据库中。

这里,@RequestBody是 Spring MVC 框架的一个注解。它在控制器中用于实现对象序列化和反序列化。它通过提取逻辑来帮助您避免样板代码。@RequestBody注解的函数返回一个与 HTTP 网络响应体绑定的值。这里的对象是UserModel

更新用户

查找updateUser()操作的代码片段:

//    Updating a User
@PutMapping(path = ["/user/{id}"])
fun updateUser(@RequestBody userModel: UserModel, @PathVariable("id") id: Int): ResponseEntity<UserModel> {
    userService.updateUser(userModel, id)
    return ResponseEntity(userModel, HttpStatus.OK)
}

@PutMapping(path = ["/user/{id}"])注解是"/user/{id}"的 URL 路径,它是一个具有特定 ID 的PUT请求。在这里,我们将更新数据库中的特定用户详细信息。

删除用户

查找deleteUser()操作的代码片段:

//    Deleting a User
@DeleteMapping(path = ["/user/{id}"])
fun deleteUser(@PathVariable("id") id: Int): String {
    userService.deleteUser(id)
    return "$id User has been deleted."
}

@DeleteMapping(path = ["/user/{id}"])注解是"/user/{id}"的 URL 路径,它是一个具有特定 ID 的删除请求。在这里,我们将从数据库中删除特定的用户详细信息。

如果您完成这个控制器类,那么您就可以运行这个应用程序并使用 Insomnia 测试 REST API 了。

测试输出

让我们运行项目。如果项目没有遇到错误,那么您将能够在 IDE 中看到 RUN 标签,如下截图所示:

现在,打开 Insomnia 应用程序。让我们在这个应用程序中应用 REST API 请求。

获取用户列表

使用此GET请求与此 URL:http://localhost:8080/users,然后点击发送。用户详细信息将从数据库中检索,您可以看到返回的 JSON 值,如下截图所示:

通过 ID 获取一个用户

使用此 URL:http://localhost:8080/user/1创建一个GET函数并点击发送。用户详细信息将从数据库中检索,您可以看到具有id1的用户返回的 JSON 值,如下截图所示:

插入新用户

使用此 URL:http://localhost:8080/user/new创建一个POST函数并点击发送。这将向数据库中插入一个用户并显示新的用户详细信息,如下截图所示:

如果您使用/usersGET请求 URL 路径,您可以检查包含新用户的用户列表:

更新用户

使用此 URL:http://localhost:8080/user/8创建一个UPDATE函数并点击发送。它将更新数据库中具有编号八的用户,并显示更新的用户信息,如下截图所示:

如果您使用http://localhost:8080/user/8GET请求 URL 路径,您可以检查具有新详细信息的新的用户,如下截图所示:

删除用户

使用此 URL 创建一个DELETE函数:http://localhost:8080/users,然后点击发送。这将从数据库中删除特定的用户,如下面的截图所示:

如果你检查所有用户,那么你会看到只有七个。

最后,我们已经创建了一个使用 JDBC 的应用程序,我们还创建了一个 REST API。如果您有任何更新,可以查看我们的 GitHub 项目。我还添加了一个包含 MySQL 代码的 SQL 文件。

Java 持久化 API

Java 持久化 APIJPA)是对象关系映射ORM)的一种方法。ORM 是一个将 Java 对象映射到数据库、表以及反向映射的系统。JPA 可以用于基于 Java 企业版和标准版的两种应用。Hibernate、TopLink、EclipseLink 和 Apache OpenJPA 是 JPA 的实现。在这些实现中,Hibernate 是最先进且最广泛使用的。

JPA 帮助开发者直接与对象工作,因此无需担心 SQL 语句。借助 JPA,他们可以将数据从关系数据库映射、存储、更新和检索到 Java 对象或反之亦然。

JPA 元数据主要由类中的注解定义。然而,它也支持 XML,这意味着它可以通过 XML 定义。在这本书中,我们将使用注解来定义 JPA 元数据。现在,我们将看到 JPA 的架构及其用途。

JPA 架构

以下图表显示了 JPA 的类级别架构:

让我们描述一下这个图表:

  • EntityManagerFactoryEntityManager的工厂类,用于创建和管理多个EntityManager实例。

  • EntityManager:这是一个接口,用于管理对象上的持久化操作。

  • Entity:这是一个以记录形式存储在数据库中的持久化对象

  • EntityTransaction:它与EntityManager有一个一对一的关系。对于每个EntityManager,操作都由EntityTransaction类维护。

  • Query:这是一个接口,每个 JPA 供应商都通过它来实现使用标准来获取关系对象。

  • Persistence:这是一个类。要获取EntityManagerFactory实例,它包含静态方法。

如果你再次查看图表,可能会注意到属于javax.presistence包的类和接口之间存在某种关系:

  • EntityManagerFactoryEntityManager之间有一个一对多关系

  • EntityManagerEntityTransaction之间有一个一对一关系

  • EntityManagerQuery之间有一个一对多关系

  • EntityManagerEntity之间有一个一对多关系

使用 JPA 创建项目

让我们使用 Spring Boot 和 JPA,以及 Hibernate 和 MySQL 创建一个简单的项目。我们将构建一个用户列表的 RESTful CRUD API。

要创建一个项目,请访问此链接:start.spring.io并创建一个基于 Kotlin 的项目。

Maven 依赖

如果你访问pom.xml文件,你可以在那里看到 JDBC 的依赖项。我们正在使用 MySQL 作为数据库:

-----
-----
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
-----
-----

根据这段代码,以下是依赖项:

  • Web

  • JPA

  • MySQL

  • H2

在这里,我们看到了一个新的依赖项名称h2。这是众所周知的一种内存数据库。Spring Boot 和 H2 之间有着很好的组合。

创建数据源

我们在application.properties中配置了DataSourceconnection pool。Spring Boot 使用spring.datasource接口作为前缀来配置 DataSource。我们的数据库模式名称是cha6_dbtest_schema。你可以自己创建它并重命名。以下是application.properties的详细信息:

## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.url = jdbc:mysql://localhost:3306/cha6_dbtest_schema?useSSL=false
spring.datasource.username = root
spring.datasource.password = 12345678

## Hibernate Properties
# The SQL dialect makes Hibernate generate better SQL for the chosen database spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect

# Hibernate ddl auto (create, create-drop, validate, update) spring.jpa.hibernate.ddl-auto = update

在我们的系统中,MySQL 的详细信息如下:

  • Host -- localhost

  • Port -- 3306

  • Username -- rootPassword -- 12345678

  • Database Name - packtpub_dbtest

  • Database Schema Name - packtpub_dbtest_schema

创建一个模型

在这个项目中,我们将创建一个 REST API 来查看用户详情列表,我们可以获取用户名、电子邮件 ID 和联系电话。所以让我们创建一个用户模型,类名为UserModel.kt

这里是模型类的代码:

@Entity
@Table(name="user_jpa")
@EntityListeners(AuditingEntityListener::class)
data class UserModel(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    var id: Long = 0,

    @NotBlank
    @Column(name = "name")
    var name: String ?= null,

    @NotBlank
    @Column(name = "email")
    var email: String ?= null,

    @NotBlank
    @Column(name = "contact_number")
    var contact_number: String ?= null
)

在这里,我们的UserModel类有以下字段:

  • id:具有自增的主键

  • name:  (非空字段)

  • email: (非空字段)

  • contact_number: (非空字段)

与 JDBC 不同,你不需要在数据库中手动创建任何表。JPA 将使用UserModel创建一个表。让我们看看如何使用这个UserModel对象在我们的数据库中创建一个表:

  • @Entity:所有你的领域模型都必须使用这个注解。这个注解用于标记类为一个持久的 Java 类。

  • @Table:这个注解用于提供表的详细信息。实体将通过它进行映射。

  • @Id:这个注解用于定义主键。

  • @GeneratedValue:这个注解用于定义主键生成策略。在前面的例子中,我们已经声明主键为一个自增字段。

  • @NotBlank:这个注解用于验证被注解的字段不为空或为空字符串。

  • @Column:这个注解用于验证将被映射到被注解字段上的列的属性。

创建用户仓库

我们将在这个仓库类中与数据库进行通信。这是一个Repository类,因此我们使用@Repository注解它。让我们创建一个名为UserRepository.ktRepository类,它扩展了JpaRepository。通过扩展JpaRepository,这个接口将获得一组通用的 CRUD 函数来创建、更新、删除和获取数据。

这里是Repository类的代码:

@Repository
interface UserRepository: JpaRepository<UserModel, Long>

从这个JPARepository中,我们将获得以下一些函数:

  • List<T> findAll(): 获取所有数据

  • List<T> findAll(Sort var1):用于获取排序后的所有数据。

  • List<T> findAllById(Iterable<ID> var1): 通过 ID 获取数据

  • <S extends T> List<S> saveAll(Iterable<S> var1): 使用数据列表插入数据

创建控制器

如果您的模型和仓库类已经完整,那么您就可以创建控制器类了,我们将创建 GetMappingPostMappingPutMappingDeleteMapping 来创建 RESTful API URL 路径。让我们使用 @RestController 注解创建一个名为 UserController.kt 的控制器类:

@RestController
class UserController {
    ----
    ----
}

自动装配仓库

让我们使用 @Autowired 注解自动装配 UserRepository。以下是这个类的代码片段:

@RestController
class UserController {

    @Autowired
 private lateinit var userRepository: UserRepository

    ----
    ----
}

获取用户列表

查找 getAllUsers() 操作的代码片段:

// to get all the users details
 @GetMapping("/users")
    fun getAllUsers(): List<UserModel>{
        return userRepository.findAll()
    }

@GetMapping(path = ["/users"]) 注解表示它用于 GET 请求。在这里,我们将使用 UserRepository 接口的 findAll() 方法从数据库获取用户列表,该接口实现了 JpaRepository。因此,我们不需要创建一个 自定义接口,与 JDBC 不同。

通过 ID 获取一个用户

查找 getAllUserByID() 操作的代码片段如下:

 // to get one specific user details
 @GetMapping("/user/{id}")
    fun getUser(@PathVariable(name = "id") id: Long): UserModel {
        return userRepository.findById(id).get()
    }

@GetMapping(path = ["/user/{id}"]) 注解是 URL 路径 "/user/{id}",它是一个具有特定 ID 的 GET 请求。在这里,我们返回 findById(id).get() 以从数据库获取特定用户详细信息。

插入新用户

查找 addNewUser() 操作的代码片段如下:

// to add a user
@PostMapping("/users")
fun addUser(@Valid @RequestBody userModel: UserModel): UserModel {
    return userRepository.save(userModel)
}

@PostMapping(path = ["/user/"]) 注解是 URL 路径 "/user/",它是一个 POST 请求。在这里,我们输入用户详细信息以将用户数据插入数据库。

为了将请求体绑定到方法参数,我们使用 @RequestBody 注解。

@Valid 注解确保请求体有效且非空。

在这里,我们返回 save(userModel) 以将新用户详细信息插入数据库。

更新用户

查找 updateUser() 操作的代码片段:

 // to update a user
    @PutMapping("/user/{id}")
    fun updateUser(@PathVariable(name = "id")id: Long, @Valid @RequestBody userDetails: UserModel): UserModel {
        val currentUser: UserModel = userRepository.findById(id).get()

        currentUser.name = userDetails.name
        currentUser.email = userDetails.email
        currentUser.contact_number = userDetails.contact_number

        return userRepository.save(currentUser)
    }

@PutMapping("/user/{id}") 注解是 URL 路径 "/user/{id}",它是一个具有特定 ID 的 PUT 请求。在这里,我们将更新数据库中的特定用户详细信息。

删除用户

查找 deleteUser() 操作的代码片段如下:

// to delete a user
 @DeleteMapping("/user/{id}")
    fun deleteUser(@PathVariable(name = "id")id: Long): ResponseEntity<*>{
        userRepository.delete(userRepository.findById(id).get())
        return ResponseEntity.ok().build<Any>()
    }

@DeleteMapping("/user/{id}") 注解是 URL 路径 "/user/{id}",它是一个具有特定 ID 的 DELETE 请求。在这里,我们将从数据库中删除特定用户详细信息。

如果您完成了这个控制器类,那么您就可以运行这个应用程序并使用 Insomnia 测试 REST API

查看输出

在运行项目之前,请转到 MySQL Workbench 应用程序,cha6_dbtest 表和 cha6_dbtest_schema。在那里,您将注意到将没有名为 user_jpa 的表,正如在 UserModel 类中提到的表名。

这是没有表的架构截图:

架构截图

让我们运行应用程序,再次检查数据库,并刷新模式。注意现在有一个表格,正如我们在UserModel@Table注解中提到的。这个表格包含该对象的所有列,包括idnameemailcontact_number

这是更新后的数据库截图:

测试系统与 JDBC 相同。请自行检查,如果您感到困惑,请访问Testing the Output of JDBC项目。

这是本项目的 REST API URL:

  • GET http://localhost:8080/users: 获取所有用户的列表

  • GET http://localhost:8080/user/1: 获取特定用户详细信息

  • POST http://localhost:8080/user/new: 插入新用户

  • PUT http://localhost:8080/user/1: 更新特定用户详细信息

  • DELETE http://localhost:8080/user/2: 删除特定用户详细信息

客户端应用程序数据库

到目前为止,你已经了解了服务器端数据库。现在我们将了解客户端数据库。Android 应用程序将是我们的客户端应用程序。Android 的需求现在正在迅速增长,并且已经超过了基于 PC 的操作系统。即使在当今,硬件也比 PC 或笔记本电脑更强大。

数据库是智能设备的核心部分,它是存储和管理设备上数据的最佳方式。这些数据可以有两种处理方式。一种方式是基于在线的,这意味着所有数据都由服务器端或云处理,而移动设备通过网络与他们通信。没有互联网连接,这个系统几乎毫无用处。第二种选择是将所有数据存储在本地数据库中。这意味着它可以在离线状态下使用,并且对互联网的依赖性也更小。

移动数据库有一些标准:

  • 轻量级和快速

  • 安全

  • 独立于在线服务器

  • 使用代码易于处理

  • 可以公开或私下共享

  • 低功耗和低内存

市场上有很多移动数据库,但满足这些标准的数据库却很少。SQLiteRealm DB 和ORMLite是其中的一些。

在这本书的整个过程中,我们将使用 SQLite 数据库。然而,我们不会使用原始的 SQLite。相反,我们将使用一个名为room 持久化库的库,它是架构组件的一部分。它为 SQLite 提供了一个抽象层。这允许更健壮的数据库访问,并有助于编写更少的代码。

架构组件

架构组件是 Android Jetpack 的组件之一。这是一份关于应用程序架构的指南。该组件基于一些库以更简单的方式执行常见任务。借助该组件,开发者可以开发他们的项目,这些项目可以更健壮、可维护和可测试。

今天我们将创建一个 Android 离线应用程序,我们将使用 Android 组件。

这是此架构的图示:

图片

以下是对所有组件的简要描述:

  • UI 控制器:活动、片段等 UI 组件位于此组件下。

  • ViewModel:这个通过模型获取数据,并将其提供给 UI。

  • LiveData:这个类持有可观察的数据。这是生命周期感知的,与常规的可观察数据不同。

  • Repository:这个用于管理多个数据源。

  • Room 数据库:这是顶层数据库层,来自 SQLite 数据库。

  • Entity:这描述了一个数据库表。

  • DAO:全称是数据访问对象DAO),它映射 SQL 查询。

  • SQLite 数据库:数据使用此在设备中存储。它由 room 创建和维护。

创建 Android 应用

让我们创建一个简单的具有数据库的 Android 应用。这将存储用户的详细信息(包括姓名、联系电话和电子邮件 ID),并使用RecyclerView在列表中显示这些详细信息:

首先,我们需要使用 Android Studio 创建一个应用,写下你的项目名称和公司域名。别忘了勾选“包含 Kotlin 支持”,使其成为一个基于 Kotlin 的应用。以下截图显示了“创建 Android 项目”窗口:

图片

现在从“手机和平板”选项中选择最低 API 版本。此项目不需要添加其他选项。在点击“添加活动到移动”中的“下一步”后,你可以选择“基本活动”,然后重命名活动名称布局,点击“完成”。在构建项目后,你将准备好开始创建 Android 应用。

这是“添加活动到移动”窗口的截图,我们选择基本活动模板,如下截图所示:

图片

此项目的最终文件如下截图所示,其中显示了完成此项目后所有文件和资源:

图片

Gradle 信息

这里是我的 Android Studio 的 Gradle 文件详情:

buildscript {
   -----
-----
    dependencies {
 classpath 'com.android.tools.build:gradle:3.2.1'
 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.10"

    }
}
-----
-----

此文件注入了 Gradle 和 Kotlin 的依赖项。在此项目中,Gradle 版本是 3.2.1,Kotlin 版本是 1.3.10

Gradle 依赖项

此 Gradle 文件是用于应用的。它包含所有依赖项和其他 Android SDK 版本。

在以下代码块中的依赖项中是以下代码:

      // Room components
    implementation "android.arch.persistence.room:runtime:$rootProject.roomVersion"
    kapt "android.arch.persistence.room:compiler:$rootProject.roomVersion"
    androidTestImplementation "android.arch.persistence.room:testing:$rootProject.roomVersion"

    // Lifecycle components
    implementation "android.arch.lifecycle:extensions:$rootProject.archLifecycleVersion"
    kapt "android.arch.lifecycle:compiler:$rootProject.archLifecycleVersion"

    // Coroutines
    api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
    api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"

要启用协程功能,请在应用的 build.gradle 文件末尾添加以下代码:

kotlin {
    experimental {
 coroutines "enable"    }
}

创建实体

让我们创建一个名为UserModel.kt的用户类,并使用@Entity注解,以便每个用户都是一个实体。所有变量列不应是私有的,这样Room就能实例化你的对象:

@Entity(tableName = "users")
class Users(): Parcelable {
    @PrimaryKey(autoGenerate = true)
    @NonNull
    @ColumnInfo(name = "userId")
    var userId: Int = 0

    @NonNull
    @ColumnInfo(name = "username")
    lateinit var username: String

    @NonNull
    @ColumnInfo(name = "email")
    lateinit var email: String

    @NonNull
    @ColumnInfo(name = "contactNumber")
    lateinit var contactNumber: String

   @NonNull
    @ColumnInfo(name = "address")
    lateinit var address: String

    constructor(username: String, email: String, contactNumber: String, address: String):this(){
        this.username = username
        this.email = email
        this.contactNumber = contactNumber
        this.address = address
    }

    override fun toString(): String {
        return "Users(username='$username', email='$email', contactNumber='$contactNumber', address='$address')"
    }
}

让我们看看这个类中有什么:

  • @Entity(tableName = "users"):一个实体类代表一个表,我们的表名是users

  • @ColumnInfo(name = "**"):这指定了表中的名称

  • @PrimaryKey(autoGenerate = true): 这意味着ID是我们的主键,并且它的值将自动增加。

  • @NonNull: 这意味着列中不会有 null 或空值。

为了将此对象从一个活动传递到另一个活动,我们需要将此类转换为Parcelable类。所以让我们扩展这个类。按照传统方式,它将需要像以下这样的大量代码:

@Entity(tableName = "users")
class Users(): Parcelable {
    ----
    ----
    constructor(parcel: Parcel) : this() {
        userId = parcel.readInt()
        username = parcel.readString()!!
        email = parcel.readString()!!
        contactNumber = parcel.readString()!!
        address = parcel.readString()!!
    }
    ----
    ----
    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeInt(userId)
        parcel.writeString(username)
        parcel.writeString(email)
        parcel.writeString(contactNumber)
        parcel.writeString(address)
    }

    override fun describeContents(): Int {
        return 0
    }

    companion object CREATOR : Parcelable.Creator<Users> {
        override fun createFromParcel(parcel: Parcel): Users {
            return Users(parcel)
        }

 override fun newArray(size: Int): Array<Users?> {
            return arrayOfNulls(size)
        }
    }
}

因此,虽然我们不需要修改重写的函数和构造函数,但理解和处理它确实很复杂。然而,如果你省略这些行,当然你会很高兴,你的代码看起来也会很漂亮。为此,我们需要应用懒人编码者的方式。

我们只需要在模型类顶部添加一个名为@Parcelize的注解。以下是完整的代码:

@Parcelize
@Entity(tableName = "users")
class Users(): Parcelable {
    @PrimaryKey(autoGenerate = true)
    @NonNull
    @ColumnInfo(name = "userId")
    var userId: Int = 0

    @NonNull
    @ColumnInfo(name = "username")
    lateinit var username: String

    @NonNull
    @ColumnInfo(name = "email")
    lateinit var email: String

    @NonNull
    @ColumnInfo(name = "contactNumber")
    lateinit var contactNumber: String

   @NonNull
    @ColumnInfo(name = "address")
    lateinit var address: String

    constructor(username: String, email: String, contactNumber: String, address: String):this(){
        this.username = username
        this.email = email
        this.contactNumber = contactNumber
        this.address = address
    }

    override fun toString(): String {
        return "Users(username='$username', email='$email', contactNumber='$contactNumber', address='$address')"
    }
}

因此,没有更多的额外代码。为了启用此功能,你需要在build.gradle (Module: app)文件的android块中添加以下代码:

android {
    ----
    ----
    androidExtensions {
        experimental = true
    }
}
dependencies {
    ----
    ----
}

创建 DAO

让我们创建一个名为UserDAO.kt的接口,并使用@DAO注解。这将帮助Room识别DAO类。以下是DAO接口的代码:

@Dao
interface UserDAO

在此接口中,我们将创建负责插入、删除和获取用户详情的函数:

@Insert
fun addNewUser(users: Users)

在前面的代码中,@Insert用于插入一个用户:

@Query("DELETE FROM USERS")
fun deleteAllUsers()

在前面的代码中,@Query("DELETE FROM USERS")用于从USERS表中删除所有用户:

@Query("SELECT * FROM USERS")
fun getAllUsers():  List<Users>

在此代码中,@Query("SELECT * FROM USERS")用于从USERS表中获取所有用户作为列表。

创建 LiveData 类

数据总是动态变化的,因此我们必须保持其更新并显示最新的结果给用户。为此,我们需要观察数据。LiveData是一个生命周期库类,可以观察数据并做出反应。

让我们将UserDao.kt中的getAllUsers()函数用LiveData包装:

@Query("SELECT * FROM USERS")
fun getAllUsers():  LiveData<List<Users>>

@Query("SELECT * FROM USERS")用于从USERS表中获取所有信息。

因此,这里是 DAO 接口的完整代码:

@Dao
interface UserDAO {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun addNewUser(users: Users)

    @Query("DELETE FROM USERS")
    fun deleteAllUsers()

    @Query("SELECT * FROM USERS")
    fun getAllUsers():  LiveData<List<Users>>
}

MainActivity中,我们将看到如何创建数据的Observer并重写观察者的onChanged()函数。

创建 Room 数据库

Room不是一个数据库,而是SQLite数据库的一层。它主要使用DAO和查询来简化客户端对数据库的获取。它不在主线程上使用,而是在后台线程上异步运行,因此 UI 性能不会下降。

让我们创建一个名为UsersRoomDatabase的抽象类并扩展RoomDatabase。使用@Database注解并指定Users类作为实体,并添加版本号。最后,初始化UserDao类的抽象函数:

@Database(entities = [Users::class], version = 1)
abstract class UsersRoomDatabase : RoomDatabase() {
    abstract fun userDAO(): UserDAO
----
----
}

让我们创建一个单例。这将处理在同时打开时数据库的多个实例。

初始化UsersRoomDatabase对象。

UsersRoomDatabase的名称是"user_database"

这是这个对象的代码片段:

// static members
companion object {
    @Volatile
    private var INSTANCE: UsersRoomDatabase? = null

    fun getDatabase(context: Context, scope: CoroutineScope): UsersRoomDatabase {
        val tempInstance = INSTANCE
        if (tempInstance != null) {
            return tempInstance
        }
        synchronized(this) {
            val instance = Room.databaseBuilder(
                context.applicationContext,
                UsersRoomDatabase::class.java,
                "user_database"
            ).addCallback(UserDatabaseCallback(scope))
                .build()
            INSTANCE = instance
            return instance
        }
    }
}

填充数据库

要在数据库中存储数据,我们可以通过使用用户的代码输入一些示例数据。其余的数据将通过使用NewUserActivity.kt类来存储。

对于示例数据,我们创建了一个简单的函数,其中插入两个示例用户详情,并在运行应用程序后显示。

要做到这一点,让我们创建一个带有CoroutineScope参数的内部回调UserDatabaseCallback()并扩展RoomDatabase.Callback()。最后,我们将重写onOpen(db: SupportSQLiteDatabase),在那里我们可以添加两个随机的用户对象:

fun populateDatabase(userDao: UserDAO) {
            userDao.addNewUser(
                Users(
                    "Sunnat", "sunnat629@gmail.com",
                    "1234567890", "Dhaka"
                )
            )
            userDao.addNewUser(
                Users(
                    "Chaity", "chaity123@gmail.com",
                    "54321987", "Dhaka"
                )
            )
        }

这里我们使用userDao.addNewUser()创建了用户详情。如果运行应用程序,这些用户详情将显示在列表视图中。

最后,我们需要将回调函数添加到数据库中,并调用build()来完成这个回调,就像以下代码所示:

fun getDatabase(context: Context, scope: CoroutineScope): UsersRoomDatabase {
    val tempInstance = INSTANCE
    if (tempInstance != null) {
        return tempInstance
    }
    synchronized(this) {
        val instance = Room.databaseBuilder(
            context.applicationContext,
            UsersRoomDatabase::class.java,
            "user_database"
        ).addCallback(UserDatabaseCallback(scope))
 .build()
        INSTANCE = instance
        return instance
    }
}

private class UserDatabaseCallback(
 private val scope: CoroutineScope
) : RoomDatabase.Callback() {

 override fun onOpen(db: SupportSQLiteDatabase) {
 super.onOpen(db)
 INSTANCE?.let { database ->
 scope.launch(Dispatchers.IO) {
 populateDatabase(database.userDAO())
 }
 }
 }
----
----
}

在前面的代码中,我们创建了一个名为UserDatabaseCallback的回调类,我们使用名为userDAO()DAO函数来填充数据库。

然后我们使用addCallback()将这个回调添加到getDatabase()函数的instance中。

实现仓库

仓库类是Room数据库和ViewModel之间的桥梁。这提供了来自多个数据源的数据,并隔离了数据层。

我们可以将这个仓库分为两个部分;一个是 DAO,主要用于本地数据库以及将本地数据库与应用程序连接起来。另一个部分是网络,主要用于处理和云与应用程序之间的通信。

现在创建一个名为UsersRepository.kt的仓库类,并将UserDAO声明为这个类的构造函数。

这里是UsersRepository.kt的代码:

class UsersRepository(private val mUserDAO: UserDAO) {

    val mAllUsers: LiveData<List<Users>> = mUserDAO.getAllUsers()

    @WorkerThread
    suspend fun insert(user: Users){
        mUserDAO.addNewUser(user)
    }
}

这里,我们已经初始化了用户列表。现在Room将执行所有查询。查询将在不同的线程上完成。

LiveData会在数据库有任何变化时通知回调函数。insert(user: Users)是用于包装addNewUser()的函数。这个插入函数必须在非 UI 线程上运行,否则应用程序会崩溃。为了避免这种情况,我们需要使用@WorkerThread注解,这有助于在非 UI 线程上执行这个函数。

创建 ViewModel

现在创建一个名为MainViewModel.ktViewModel类。

这里是MainViewModel.kt类:

open class MainViewModel(application: Application) : AndroidViewModel(application) {
    private val mRepository: UsersRepository
    private val mAllUsers: LiveData<List<Users>>

    private var  parentJob = Job()
    private val coroutineContext: CoroutineContext
        get() = parentJob + Dispatchers.Main

    private val scope = CoroutineScope(coroutineContext)

    init {
        val userDao = UsersRoomDatabase.getDatabase(application, scope).userDAO()
        mRepository = UsersRepository(userDao)
        mAllUsers = mRepository.mAllUsers
    }

    fun getAllUsers(): LiveData<List<Users>>{
        return mAllUsers
    }

    fun insert(users: Users) = scope.launch(Dispatchers.IO){
        mRepository.insert(users)
    }

    override fun onCleared() {
        super.onCleared()
        parentJob.cancel()
    }
}

这个类将Application作为参数获取并扩展了AndroidViewModel

初始化一个WordRepository的私有变量和一个LiveData,这将缓存用户列表。

init块中,从UsersRoomDatabase添加一个UserDAO引用,并将mAllUsers初始化为mRepository.mAllUsers

创建新的活动

现在我们需要一个活动,我们将在这里创建一个函数来插入用户详情并将其保存到数据库中。在 app 文件夹上右键单击,创建一个名为NewUserActivity.kt的空活动,如下截图所示:

截图

下面是这个名为activity_new_user.xml的布局类的代码。(完整代码可以在 GitHub 链接中找到):

----
----
    <EditText
            android:id="@+id/editEmail"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/hint_email"
            android:inputType="textEmailAddress"
            android:padding="5dp"
            android:textSize="18sp" android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/editUsername" app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"/>

    <EditText
            android:id="@+id/editContactID"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/hint_contact"
            android:inputType="phone"
            android:padding="5dp"
            android:textSize="18sp" android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/editEmail" app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
    />
----
---
    <Button
            android:id="@+id/buttonSave"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/colorPrimary"
            android:text="@string/button_save"
            android:textColor="@android:color/white"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/editAddress" app:layout_constraintVertical_bias="1.0"/>
</android.support.constraint.ConstraintLayout>

在这里,我们添加了四个EditText输入框,可以输入用户名联系电话电子邮件地址,以及一个名为buttonSave的按钮来将此信息保存到数据库中。

下面是NewUserActivity.kt类的代码:

class NewUserActivity : AppCompatActivity(), View.OnClickListener {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_new_user)
        buttonSave.setOnClickListener(this)
    }

    override fun onClick(view: View?) {
        if (view!!.id == R.id.buttonSave){
            val intent = Intent()
            if (isTextFieldEmpty()){
                Snackbar.make(view, "Empty Field", Snackbar.LENGTH_LONG)
                    .setAction("Action", null).show()
                setResult(Activity.RESULT_CANCELED, intent)
            } else {
                val users = Users(editUsername.text.toString(),
                    editEmail.text.toString(),
                    editContactID.text.toString(),
                    editAddress.text.toString())

                Log.wtf("CRAY", editUsername.text.toString()+" "+
                        editEmail.text.toString()+" "+
                        editContactID.text.toString()+" "+
                        editAddress.text.toString())

                Log.wtf("CRAY", users.toString())
                // If an instance of this Activity already exists, then it will be moved to the front.
                // If an instance does NOT exist, a new instance will be created.
                intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
                intent.putExtra(getString(R.string.result_replay), users)
                setResult(Activity.RESULT_OK, intent)
            }
            finish()
        }
    }

    private fun isTextFieldEmpty(): Boolean {
        return TextUtils.isEmpty(editUsername.text) ||
                TextUtils.isEmpty(editEmail.text) ||
                TextUtils.isEmpty(editContactID.text) ||
                TextUtils.isEmpty(editAddress.text)
    }
}

根据前面的代码:

  • 实现View.OnClickListener并重写onClick(view: View?)

  • onCreate()方法中,为buttonSave按钮设置setOnClickListener(),并重写我们想要与按钮一起执行的onClick(view: View?)方法。最后,我们调用一个Intent,这将使活动从**UserModel**切换到MainActivity类。

  • isTextFieldEmpty()函数用于检查EditText字段是否为空。

  • 然后我们获取所有文本,创建一个UserObject,并使用intent.putExtra(getString(R.string.result_replay), users)将这个可序列化的用户对象传递给MainActivity

创建自定义 RecyclerView 适配器

为了显示所有用户列表,我们将使用RecyclerView。对于我们的项目,我们需要以我们自己的方式自定义RecyclerView适配器。在这个适配器中,我们主要传递用户模型。这将显示用户名、电子邮件和联系电话。让我们创建一个名为UserListAdapter.kt的适配器并扩展RecyclerView.Adapter<UserListAdapter.UserViewHolder>()。以下是UserListAdapter.kt的代码:

class UserListAdapter internal constructor(context: Context) :
    RecyclerView.Adapter<UserListAdapter.UserViewHolder>() {

    private val mLayoutInflater: LayoutInflater = LayoutInflater.from(context)!!
    private var mUsers: List<Users> = emptyList() // Cached copy of users

    inner class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val rowName: TextView = itemView.name
        val rowEmail: TextView = itemView.email
        val rowContactNumber: TextView = itemView.contactNumber
        val rowAddress: TextView = itemView.contactNumber
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        val itemView: View = mLayoutInflater.inflate(R.layout.recyclerview_item, parent, false)
        return UserViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        holder.rowName.text = mUsers[position].username
        holder.rowEmail.text = mUsers[position].email
        holder.rowContactNumber.text = mUsers[position].contactNumber
        holder.rowAddress.text = mUsers[position].address
    }

    override fun getItemCount(): Int {
        return mUsers.size
    }

    internal fun setNewUser(users: List<Users>) {
        mUsers = users
        notifyDataSetChanged()
    }
}

根据代码:

onCreateViewHolder()
onBindViewHolder()
UserViewHolder()

在这里,我们在UserViewHolder内部类中初始化了activity_new_user.xml的四个属性:

val rowName: TextView = itemView.name
val rowEmail: TextView = itemView.email
val rowContactNumber: TextView = itemView.contactNumber
val rowAddress: TextView = itemView.contactNumber

我们在onBindViewHolder()函数中设置了userModel的这四个属性值,如下所示:

holder.rowName.text = mUsers[position].username
holder.rowEmail.text = mUsers[position].email
holder.rowContactNumber.text = mUsers[position].contactNumber
holder.rowAddress.text = mUsers[position].address

实现 RecyclerView

RecyclerView是一个列表,我们可以看到所有用户列表。RecyclerView是设计材料的一部分,有助于使列表更加平滑且快速加载数据。

MainActivity中,我们在onCreate()函数中设置RecycleView,如下所示:

val userListAdapter = UserListAdapter(this)
recyclerview.adapter = userListAdapter
recyclerview.layoutManager =  LinearLayoutManager(this)

修改主活动

让我们修改这个MainActivity类来完成我们的项目。让我们首先将 UI 连接到数据库。我们将使用RecyclerView来显示数据库中的数据列表。

让我们创建一个ViewModel变量,如下所示:

private lateinit var mMainViewModel: MainViewModel

使用ViewModelProvidersMainViewModelMainActivity连接。在onCreate()中,我们将从ViewModelProvider获取ViewModel,如下所示:

mMainViewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)

要添加LiveData观察者,让我们添加以下observe()来观察getAllUsers(),如下所示:

mMainViewModel.getAllUsers().observe(this,
    Observer {
            userList -> userListAdapter.setNewUser(userList!!)
    })

从另一个活动获取数据

创建新活动部分中,我们提到已经将可序列化的用户对象传递给了MainActivity。为了获取这个对象,我们需要创建一个请求码。让我们创建一个如下所示的请求码:

private val requestCode: Int = 1

现在,重写onActivityResult()函数,我们将从中检索传递的NewUserActivity对象。

下面是onActivityResult()函数的代码:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == this.requestCode && resultCode == Activity.RESULT_OK){
        data?.let {
        val users: Users = it.getParcelableExtra(getString(R.string.result_replay)) as Users
        mMainViewModel.insert(users)
        }
    }
}

getParcelableExtra()用于检索Parcelable对象。然后我们调用mMainViewModel.insert(users)将返回的User插入到数据库中。

添加 XML 布局

content_main.xml中,我们添加了RecyclerView。这是这个布局的代码:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        tools:showIn="@layout/activity_main"
        tools:context=".ui.MainActivity">
    <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerview"
            android:background="@android:color/darker_gray"
            tools:listitem="@layout/recyclerview_item"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_height="0dp" android:layout_width="0dp"/>
</android.support.constraint.ConstraintLayout>

切换到另一个活动

activity_main.xml中,我们添加了一个FloatingActionButton,我们将使用它来进入NewUserActivity。为了完成这个任务,在onCreate()中使用以下代码,并指定提到的请求代码:

fab.setOnClickListener {
    val intent = Intent(this@MainActivity, NewUserActivity::class.java)
    startActivityForResult(intent, requestCode)

    /*Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
        .setAction("Action", null).show()*/
}

因此,这是MainAcivity.kt的完整代码:

class MainActivity : AppCompatActivity() {

    private val requestCode: Int = 1

    private lateinit var mMainViewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)

        val userListAdapter = UserListAdapter(this)
        recyclerview.adapter = userListAdapter
        recyclerview.layoutManager =  LinearLayoutManager(this)

        mMainViewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        mMainViewModel.getAllUsers().observe(this,
            Observer {
                    userList -> userListAdapter.setNewUser(userList!!)
            })

        fab.setOnClickListener {
            val intent = Intent(this@MainActivity, NewUserActivity::class.java)
            startActivityForResult(intent, requestCode)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == this.requestCode && resultCode == Activity.RESULT_OK){
            data?.let {
            val users: Users = it.getParcelableExtra(getString(R.string.result_replay)) as Users
            mMainViewModel.insert(users)
            }
        }
    }
}

现在我们已经完成了项目,运行应用程序。我们将在下一节中探讨这一点。

运行应用

在你的 Android 设备或模拟器上运行应用后,你会看到这个屏幕:

我们可以在这里看到我们预先添加的用户详细信息。现在点击浮动按钮,进入新用户活动页面,在那里你可以写下用户信息,如图所示:

最后,点击保存按钮。你现在可以看到新的用户名,如图中所示为Naruto

因此,这样我们就学会了如何使用Room进行本地数据库。在下一章中,你将看到这个库在 Android 应用程序中的更多使用。

摘要

数据库本身是一个大型平台,我们已经涵盖了与我们的 Spring 和 Android 项目及内容相关的部分。在本章中,我们学习了数据库是什么,以及查看其各种类型。我们看到了 DBMs 的简要描述。之后,我们学习了 JDBC,它是一个连接和从前端到后端移动数据的 API 规范。然后我们使用 JDBC 开发了一个项目,在该项目中,我们从数据库中创建、读取、更新和删除数据。在这个主题之后,我们学习了另一个名为 JPA 的 API,它是一种 ORM 方法,以及一个将 Java 对象映射到数据库表并反之亦然的系统。然后我们通过一个项目学习了更多关于 JPA 及其使用的内容。在那里,我们还学习了基于 CRUD 的 REST API。最后,我们学习了 Android 的最新技术,称为架构组件。我们还查看了一个名为Room的组件,它是 SQLite 数据库顶级封装。最后,我想重申,这一章并没有解释所有内容。如果你想了解更多关于数据库的信息,你可以阅读我们推荐的书籍,我们已经在进一步阅读部分提到了书籍和作者的名字。在下一章中,你可以了解并发性,这意味着程序、算法或问题的不同单元的能力。

问题

  1. Spring Boot 中的 H2 是什么?

  2. REST API 中的资源是什么?

  3. CRUD 的全称是什么?

  4. DAO 和仓库之间的区别是什么?

  5. 什么是 SQLite?

  6. SQLite 支持哪些数据类型?

  7. 标准的 SQLite 命令有哪些?

  8. SQLite 的缺点是什么?

进一步阅读

第七章:并发

并发是程序或算法能够被划分为可以无序执行而不影响结果的部分的能力。这种方法允许在多核环境中进行并行执行,这可以显著提高性能。理解并发与并行之间的区别很重要。并行假设程序是以并发方式实现的,但并发并不意味着程序是并行执行的。

本章将涵盖以下主题:

  • 协程

  • 顺序操作

  • 回调地狱

  • 线程池

技术要求

要运行本章中的代码,你需要集成coroutines-core库。为此,你应该将以下行添加到build.gradle文件的repositories块中:

jcenter()

你还应该在dependencies块中添加以下行:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.30.2'

添加以下行以集成kotlinx-coroutines-android库:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.30.2'

如果你使用的是低于 1.3 的 Kotlin 版本,你还应该在build.gradle文件中添加以下行:

kotlin {
    experimental {
        coroutines "enable"
    }
}

要集成 Spring Android 库,你应该添加以下行:

implementation 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.8.6'

你还应该添加repositories块,如下所示:

repositories {
    maven {
        url 'https://repo.spring.io/libs-milestone'
    }
}

本章还将与JSON to Kotlin Class插件一起工作。要安装此插件,请打开首选项窗口并选择插件部分。

点击安装按钮并重新启动 Android Studio。

本章的源代码,包括示例,可在以下链接的 GitHub 上找到:github.com/PacktPublishing/Learn-Spring-for-Android-Application-Development/tree/master/Chapter07

协程

协程是 Kotlin 编程语言的一个强大功能。其主要目标是允许在等待另一个函数调用的长期操作结果时挂起函数。这个功能允许我们以顺序方式编写异步代码,而不需要回调。

本节将涵盖以下主题:

  • 协程基础

  • 调用栈

  • 协程测试

  • 协程作用域

协程基础

如果你熟悉线程的概念,你会知道每个线程都有自己的调用栈。我们将在下一节中介绍线程的调用栈。创建新线程是一个复杂的操作,大约需要两兆内存。协程在底层使用线程池,并且只需要创建几个额外的方法和类。这就是为什么你可以将协程视为轻量级线程。

让我们想象一下,我们有一个长期操作,如下面的代码所示:

class Image

fun loadImage() : Image {
    Thread.sleep(3000)
    return Image()
}

loadImage 函数需要三秒钟并返回 Image 类的实例。我们还有一个 showImages 函数,它接受三个 Image 类的实例,如下所示:

fun showImages(image1: Image, image2: Image, image3: Image) {
    // .......
}

因此,我们有三个可以并行执行的独立任务。我们在这里可以创建三个协程,每个协程将执行 loadImage 函数。要创建一个新的协程,我们可以使用称为协程构建器的函数之一,例如 asynclaunch

val subTask1 = GlobalScope.async { loadImage() }
val subTask2 = GlobalScope.async { loadImage() }
val subTask3 = GlobalScope.async { loadImage() }

async 函数返回一个 Deferred 类型的实例。这个类封装了一个将在未来返回结果的任务。当调用 Deferred 类实例的 await 函数时,caller 函数会暂停。这意味着具有此函数调用栈的线程不会被阻塞,只是被挂起。以下代码片段显示了这可能看起来像什么:

showImages(subTask1.await(), subTask2.await(), subTask3.await())

当我们调用 await 函数时,我们挂起当前函数的调用。此外,当所有子任务返回结果时,将调用 showImages 函数。

以下图显示了这些函数的执行方式:

图片

此图显示,根据核心之间的负载分配以及是否在所有三张图片都加载完毕时调用 showImages 函数,三个任务可以几乎并行执行。

调用栈

每个协程和线程都有自己的调用栈。这意味着协程或线程创建时,会同时创建其调用栈。调用栈包含每个使用此线程或协程的上下文调用的函数的类似块。这个块代表一个包含元数据、原始局部变量和堆中对象的局部引用的内存空间。您可以将调用栈视为为线程或协程分配的内存的一部分。

以下图显示了线程或协程创建时调用栈的外观:

图片

如果 main() 函数调用另一个函数,则会在调用栈中添加一个新的块。这看起来如下:

图片

loadImage 函数向 main 函数返回一个值时,loadImage 函数的块将从栈中移除。

协程测试

runBlocking 协程构建器可用于测试。它创建一个使用当前线程的协程。JUnit 框架中的测试可能如下所示:

class ExampleUnitTest {

    @Test
    fun comicLoading() = runBlocking {
        val image = async { loadImage() }.await()
        assertNotNull(image)
    }
}

此代码片段使用 async 协程构建器加载图像,并检查 image 是否不为空。runBlocking 函数的源代码如下所示:

@Throws(InterruptedException::class)
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
    val currentThread = Thread.currentThread()
    val contextInterceptor = context[ContinuationInterceptor]
    val privateEventLoop = contextInterceptor == null // create private event loop if no dispatcher is specified
    val eventLoop = if (privateEventLoop) BlockingEventLoop(currentThread) else contextInterceptor as? EventLoop
    val newContext = GlobalScope.newCoroutineContext(
        if (privateEventLoop) context + (eventLoop as ContinuationInterceptor) else context
    )
    val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop, privateEventLoop)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    return coroutine.joinBlocking()
}

如您所见,runBlocking 协程构建器使用 currentThread 函数来获取 Thread 类的实例。当您运行此测试时,您将看到以下窗口:

图片

此窗口显示测试已成功通过。

协程作用域

在协程版本 0.26.0 的发布中,引入了一个新的、重要的功能——协程作用域。coroutines-core 库中的所有协程构建器都是 CoroutineScope 接口的扩展函数。

CoroutineScope 接口如下所示:

public interface CoroutineScope {

    @Deprecated(level = DeprecationLevel.HIDDEN, message = "Deprecated in favor of top-level extension property")
    public val isActive: Boolean
        get() = coroutineContext[Job]?.isActive ?: true

    public val coroutineContext: CoroutineContext
}

我们需要协程作用域为我们启动的应用程序中的协程提供适当的取消机制。现代框架,如 Android SDK 或 React Native,都是这样构建的,即所有组件以及应用程序本身都有生命周期。在 Android SDK 中,这可以是活动或片段,在 React Native 中,这可以是组件。

协程作用域代表一个具有生命周期的对象的作用域,例如活动或组件。coroutines-core 库为整个应用程序提供了一个作用域,如果我们想启动一个与应用程序运行时间一样长的协程,我们可以使用它。整个应用程序的作用域由 GlobalScope 对象表示,如下所示:

object GlobalScope : CoroutineScope {

    @Deprecated(level = DeprecationLevel.HIDDEN, message = "Deprecated in favor of top-level extension property")
    override val isActive: Boolean
        get() = true

    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

让我们创建一个新的活动,并为其创建一个自己的协程作用域。最简单的方法是调用包的上下文菜单并选择“新建”部分,如下所示:

图片

然后,在“活动”子部分中选择“空活动”选项,如下所示:

图片

Android Studio 将打开“配置活动”窗口,您可以在其中更改活动配置并按下“完成”按钮:

图片

新创建的 XKCDActivity 类将如下所示:

class XKCDActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_xkcd)
    }
}

如果我们想从这个类中启动一个生命周期感知的协程,我们应该实现 CoroutineScope 接口,如下所示:

class XKCDActivity : AppCompatActivity(), CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_xkcd)
    }
}

CoroutineScope 接口如下所示:

public interface CoroutineScope {

    @Deprecated(level = DeprecationLevel.HIDDEN, message = "Deprecated in favor of top-level extension property")
    public val isActive: Boolean
        get() = coroutineContext[Job]?.isActive ?: true

    public val coroutineContext: CoroutineContext
}

XKCDActivity 类实现了 CoroutineScope 接口并重写了 coroutineContext 属性。重写的 coroutineContext 属性包含一个返回 Dispatchers.Main 的获取器。

Dispatchers 是来自 coroutines-core 库的对象,其中包含以下调度器:

  • Default 被所有标准协程构建器使用,例如 launchasync

  • Main 用于在主线程上运行协程

  • Unconfident 立即在一个可用的线程上调用协程

  • IO 用于运行执行输入/输出操作的协程

由于重写的 coroutineContext 属性的获取器返回 Main 调度器,因此从这个类中所有协程构建器启动的协程都将运行在主线程上。

XKCDActivity 有自己的协程作用域,但它不是生命周期感知的。这意味着如果在活动的作用域中启动了一个协程,当活动被销毁时,该协程不会被销毁。我们可以通过以下方式修复这个问题:

class XKCDActivity : AppCompatActivity(), CoroutineScope {
    private lateinit var lifecycleAwareJob: Job
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + lifecycleAwareJob

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_xkcd)
        lifecycleAwareJob = Job()
    }

    override fun onDestroy() {
        super.onDestroy()
        lifecycleAwareJob.cancel()
    }
}

lifecycleAwareJob将被用作所有协程的父级,并在活动被销毁时取消所有子协程。以下示例代码显示了如何使用这种方法:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_xkcd)
    lifecycleAwareJob = Job()
    launch {
        val image = async(Dispatchers.IO) { loadImage() }.await()
        showImage(image)
    }
}

协程启动构建器创建一个在主线程上工作的协程,而异步协程构建器创建一个在输入/输出线程上工作的协程。当image准备好时,它将在应用程序的主线程上显示。如果我们按下返回按钮,协程将被销毁,包括XKCDActivity

通道

async函数返回一个Deferred类的实例,允许我们计算单个值。如果我们需要在协程之间传输一系列值,我们可以使用通道。

通道是一个如下所示的接口:

public interface Channel<E> : SendChannel<E>, ReceiveChannel<E> {
    //.....
}

SendChannel接口如下所示:

public interface SendChannel<in E> {

    @ExperimentalCoroutinesApi
    public val isClosedForSend: Boolean

    @ExperimentalCoroutinesApi
    public val isFull: Boolean

    public suspend fun send(element: E)

    public val onSend: SelectClause2<E, SendChannel<E>>

    public fun offer(element: E): Boolean

    public fun close(cause: Throwable? = null): Boolean

    @ExperimentalCoroutinesApi
    public fun invokeOnClose(handler: (cause: Throwable?) -> Unit)
}

SendChannel接口包含一个接受参数并将其添加到该通道的send方法。如果此通道已经包含一个值,则isFull属性为true。在这种情况下,send函数会暂停调用者,直到包含的值被消费。

一个通道可以通过调用close方法来关闭。在这种情况下,isClosedForSend属性为true,而send方法会抛出一个异常。

虽然SendChannel接口允许我们将一个值放入通道中,但ReceiveChannel接口允许我们从通道中获取值。ReceiveChannel接口如下所示:

public interface ReceiveChannel<out E> {

    @ExperimentalCoroutinesApi
    public val isClosedForReceive: Boolean

    @ExperimentalCoroutinesApi
    public val isEmpty: Boolean

    public suspend fun receive(): E

    public val onReceive: SelectClause1<E>

    @ExperimentalCoroutinesApi
    public suspend fun receiveOrNull(): E?

    @ExperimentalCoroutinesApi
    public val onReceiveOrNull: SelectClause1<E?>

    public fun poll(): E?

    public operator fun iterator(): ChannelIterator<E>

    public fun cancel(): Boolean

    @ExperimentalCoroutinesApi
    public fun cancel(cause: Throwable? = null): Boolean
}

receiveOrNull()方法从该通道返回并移除一个元素,或者如果isClosedForReceive属性为true,则返回 null。ReceiveChannel包含iterator方法,可以在for循环中使用。

让我们看看以下示例代码:

fun channelBasics() = runBlocking<Unit> {
    val channel = Channel<Int>()
    launch {
        println("send 0 ${Date().toGMTString()}")
        channel.send(0)
        delay(1000)
        println("send 1 ${Date().toGMTString()}")
        channel.send(1)
    }
    delay(3000)
    val theFirstElement = channel.receive()
    println("receive $theFirstElement ${Date().toGMTString()}")
    delay(4000)
    val theSecondElement = channel.receive()
    println("receive $theSecondElement ${Date().toGMTString()}")
}

在前面的示例中,我们通过通道发送了两个值并接收了这些值。我们还使用了delay函数来显示操作需要一些时间。

输出如下所示:

send 0 21 Oct 2018 13:30:12 GMT
 receive 0 21 Oct 2018 13:30:15 GMT
 send 1 21 Oct 2018 13:30:16 GMT
 receive 1 21 Oct 2018 13:30:19 GMT

此输出显示send函数会暂停协程,直到值被消费。

我们可以使用for循环从通道接收值,如下所示:

fun channelIterator() = runBlocking<Unit> {
    val channel = Channel<Int>()
    launch {
        (0..5).forEach {
            channel.send(it)
        }
    }
    for (value in channel) {
        println(value)
    }
}

输出如下所示:

 0
 1
 2
 3
 4
 5

生成函数

producer函数被称为通道构建器,它返回一个ReceiveChannel类的实例。此函数如下所示:

@ExperimentalCoroutinesApi
public fun <E> CoroutineScope.produce(
    context: CoroutineContext = EmptyCoroutineContext,
    capacity: Int = 0,
    block: suspend ProducerScope<E>.() -> Unit
): ReceiveChannel<E> {
    val channel = Channel<E>(capacity)
    val newContext = newCoroutineContext(context)
    val coroutine = ProducerCoroutine(newContext, channel)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    return coroutine
}

如前所述的代码片段所示,produce函数包含一个ProducerScope类型的接收参数。ProducerScope接口如下所示:

public interface ProducerScope<in E> : CoroutineScope, SendChannel<E> {
    val channel: SendChannel<E>
}

如您所见,ProducerScope接口扩展了SendChannel接口。这意味着我们可以在传递给producer函数的 lambda 表达式中使用send方法。

使用producer函数的一个示例可能如下所示:

suspend fun numbersProduce(): ReceiveChannel<Int> = GlobalScope.produce {
    launch {
        (0..10).forEach {
            send(it)
        }
    }
}

我们可以这样使用numbersProduce函数:

fun producerExample() = runBlocking<Unit> {
    val numbers = numbersProduce()
    for (value in numbers) {
        println(value)
    }
}

演员(actor)函数

actor函数包含一个ActorScope类型的接收参数。actor函数的源代码如下所示:

public fun <E> CoroutineScope.actor(
    context: CoroutineContext = EmptyCoroutineContext,
    capacity: Int = 0,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    onCompletion: CompletionHandler? = null,
    block: suspend ActorScope<E>.() -> Unit
): SendChannel<E> {
    val newContext = newCoroutineContext(context)
    val channel = Channel<E>(capacity)
    val coroutine = if (start.isLazy)
        LazyActorCoroutine(newContext, channel, block) else
        ActorCoroutine(newContext, channel, active = true)
    if (onCompletion != null) coroutine.invokeOnCompletion(handler = onCompletion)
    coroutine.start(start, coroutine, block)
    return coroutine
}

ActorScope接口看起来与ProducerScope接口相似,但它实现了ReceiveChannel接口:

public interface ActorScope<E> : CoroutineScope, ReceiveChannel<E> {
    val channel: Channel<E>
}

如你所知,从不同的协程中访问可变数据并不是一个好主意。为了处理这个问题,我们可以使用通道和actor函数,如下所示:

suspend fun numberConsumer() = GlobalScope.actor<Int> {
    var counter = 0
    for (value in channel) {
        counter += value
        println(counter)
    }
}

前面的代码片段包含一个名为counter的可变变量。我们在通道接收到新值时更改counter变量的值。由于通道会挂起调用者直到消费者完成当前值的处理,我们可以确保counter变量将以正确的方式被修改。

可以这样使用numbersCounter函数:

@Test
fun actorExample() = runBlocking<Unit> {
    val actor = numberConsumer()
    (0..10).forEach {
        launch {
            actor.send(it)
        }
    }
}

前面的代码片段启动了十个协程,它们并行地向一个 actor 发送一个值。

输出如下所示:

 0
 1
 3
 6
 10
 15
 21
 28
 36
 45
 55

输出显示counter变量被正确修改。

顺序操作

协程方法最重要的好处之一是保证函数的调用顺序与它们编写的顺序相同。在多线程环境中执行并发代码时,操作顺序是一个非常重要的细节。

让我们假设我们必须使用以下函数来加载用户的详细信息:

suspend fun loadUserDetails(): User {
    delay(3000)
    return User(0, "avatar")
}

loadUserDetails函数从coroutines-core库调用delay函数,并返回User类的一个实例。delay函数挂起当前协程的调用。当用户准备好时,我们必须将avatar属性的值传递给loadImage函数:

suspend fun loadImage(avatar: String): Image {
    delay(3000)
    return Image()
}

loadImage函数还调用了delay函数,并返回Image类的一个实例。然后我们应该将接收到的Image类实例传递给showImage函数。

以下代码展示了如何使用协程依次执行这些函数:

fun main(args: Array<String>) = runBlocking {
    val user = async { loadUserDetails() }.await()
    val image = async { loadImage(user.avatar) }.await()
    showImage(image)
}

前面的代码片段依次调用了使用不同协程的三个函数。以下图显示了函数调用的顺序:

回调地狱

你应该使用协程的主要原因之一是避免回调地狱。

本节将涵盖以下主题:

  • 什么是回调?

  • 包装回调

什么是回调?

回调是一种用于检索异步任务结果的模式。这种方法假设我们传递一个函数的引用,当异步操作完成时应该调用该函数。

我们所说的同步操作是指任务一个接一个地执行。异步方法假设可以并行执行多个任务。

在以下示例代码中,loadImage函数使用回调来返回结果:

fun loadImage(callback: (Image) -> Unit) {
    executor.submit {
        Thread.sleep(3000)
        callback(Image())
    }
}

前面的代码片段展示了如何创建一个异步函数的最简单示例,该函数使用回调返回结果。在我们的情况下,回调是一个接受Image类实例并返回Unit的 lambda。以下图显示了这一序列是如何工作的:

图片

此函数可以这样使用:

fun main(args: Array<String>) {
    loadImage { image ->
        showImage(image)
    }
}

前面的代码片段显示,使用回调处理异步代码非常简单。我们只需实现并传递一个在图像准备就绪时被调用的 lambda。

以下图显示了如何实现这种方法:

图片

让我们假设我们正在从服务器请求用户列表。之后,我们发送另一个请求以获取关于用户的详细信息,然后加载一个头像。在代码中,这可能看起来如下:

fun loadListOfFriends(callback: (List<ShortUser>) -> Unit) {
    executor.submit {
        Thread.sleep(3000)
        callback(listOf(ShortUser(0), ShortUser(1)))
    }
}

loadListOfFriends函数接受一个 lambda,该 lambda 接受ShortUser类实例的列表,如下所示:

fun loadUserDetails(id: Int, callback: (User) -> Unit) {
    executor.submit {
        Thread.sleep(3000)
        callback(User(id, "avatar"))
    }
}

loadUserDetails函数接受一个 lambda 和一个用户的标识符,如下所示:

fun loadImage(avatar: String, callback: (Image) -> Unit) {
    executor.submit {
        Thread.sleep(3000)
        callback(Image())
    }
}

loadImage函数接受头像的路径和 lambda。以下示例代码演示了当我们使用带有回调的方法时最常见的常见问题。当并发任务需要相互传递数据时,我们遇到了代码复杂性和可读性的问题:

fun main(args: Array<String>) {
    loadListOfFriends {users ->
        loadUserDetails(users.first().id) {user ->
            loadImage(user.avatar) {image ->
                showImage(image)
            }
        }
    }
}

前面的代码片段演示了回调地狱。我们有很多嵌套的函数,维护这样的代码很困难。

线程池

创建新线程是一个复杂的操作,需要占用大量资源。在调用栈部分,我们介绍了为新线程分配内存的方法。当函数的较低部分从栈中移除时,线程将被销毁。为了避免不断创建新线程,我们可以使用线程池。为每个短期操作创建新线程没有逻辑,因为这种操作和将程序流程切换到创建的上下文可能比执行任务本身花费更多时间。线程池模式假设一个包含一组等待新任务的线程的类,以及一个包含任务的队列。

以下图显示了这是如何工作的:

图片

前面的图显示,池包含一个队列,该队列保存由生产者提交的任务。池中的线程从队列中取出任务并执行它们。

协程在底层使用线程池。java.util.concurrent包提供了创建自己的线程池的功能。Executors类包含许多静态工厂函数来创建一个池,如下面的截图所示:

图片

以下示例代码演示了如何创建和使用单线程执行器:

fun main(args: Array<String>) {
    val executor = Executors.newSingleThreadExecutor()
    executor.submit { loadImage() }
    executor.submit { loadImage() }
}

在前面的代码片段中,我们实例化了executor变量,并使用submit方法将任务添加到队列中。

摘要

在本章中,我们探讨了并发以及多线程环境中可能出现的问题。我们介绍了协程的常见用法示例。我们还熟悉了诸如线程池和回调等模式,以及如何使用它们。此外,我们还涵盖了同步和异步编程,以及与这些主题相关的问题。

在下一章中,我们将概述响应式编程,这在我们需要处理异步操作时非常有用。

问题

  • 什么是调用栈?

  • 什么是线程池?

  • 什么是回调?

  • 为什么协程被称为轻量级线程?

进一步阅读

由 Igor Kucherenko 所著,Packt Publishing 出版的《掌握 Kotlin 高性能编程》(www.packtpub.com/application-development/mastering-high-performance-kotlin)。

第八章:响应式编程

响应式编程是一种处理异步事件的异步方法。我们经常遇到异步事件,例如用户与界面的交互或长期操作结果的交付。还有一些库,如 RxJavaReactor, 允许我们在 Kotlin 或 Java 中编写响应式代码。

在本章中,你将了解观察者模式,以及如何将异步事件从一种类型转换为另一种类型。你还将学习如何使用实现响应式编程概念的 Mono、Single、Observable 和 Flux 类。

本章将涵盖以下主题:

  • 使用 Spring Reactor 进行响应式编程

  • 阻塞和非阻塞

  • RxJava

  • Android 中的 RxJava

到本章结束时,你将能够使用 RxJava 和 Reactor 库将响应式编程应用于你的应用程序。

技术要求

你可以在 GitHub 上找到本章的示例,链接如下:github.com/PacktPublishing/Learn-Spring-for-Android-Application-Development/tree/master/app/src/main/java/com/packt/learn_spring_for_android_application_development/chapter8

要将 Reactor 库集成到你的项目中,请在 build.gradle 文件的仓库部分添加以下行:

maven { url 'https://repo.spring.io/libs-milestone' }

请在依赖项部分添加以下行:

implementation "io.projectreactor:reactor-core:3.2.2.RELEASE"

Reactor 库与 Java 开发工具包(JDK)的 8 或更高版本兼容。因此,我们应该在 Android 部分添加以下行:

compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

要集成 RxJava 库,请在依赖项部分添加以下行:

implementation "io.reactivex.rxjava2:rxjava:2.2.3"

要集成 RxAndroid 库,请在依赖项部分添加以下行:

implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'

要集成 RxBinding 库,你应该在依赖项部分添加以下行:

implementation 'com.jakewharton.rxbinding3:rxbinding:3.0.0-alpha1'

使用 Spring Reactor 进行响应式编程

Reactor 是一个为 JVM 实现响应式编程概念的库。这种方法基于观察者模式,并提供可以发出 一个 或一系列值的类型。

在本节中,你将学习以下内容:

  • 如何实现观察者模式

  • 如何使用 Flux 发布者

  • 如何使用 Mono 发布者

观察者模式

观察者模式 假设存在一个发送消息的对象,以及另一个接收消息的对象。以下图表展示了如何组织类层次结构以实现此方法:

Activity类实现了OnClickListener接口并包含一个Button类的实例,而Button类包含调用OnClickListener类实例的onClick方法的performClick方法,如果它不为空。然后活动中的onClick方法将被调用。这样,当用户点击按钮时,Activity类的实例将得到通知。

以下示例代码展示了这种方法是如何工作的。

ObserverActivity包含一个Button类的实例并调用setOnClickListener方法:

class ObserverActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_observer)
        findViewById<Button>(R.id.button).setOnClickListener {
            Toast.makeText(this, "Clicked!", Toast.LENGTH_LONG).show()
        }
    }
}

setOnClickListener方法如下所示:

public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

performClick方法调用onClick函数,如下所示:

public boolean performClick() {
    ////......
    final boolean result;
    final ObserverInfo li = mObserverInfo;
 if (li != null && li.mOnClickObserver != null) {
 playSoundEffect(SoundEffectConstants.CLICK);
 li.mOnClickObserver.onClick(this);
 result = true;
 } else {
 result = false;
 }
 ///........
 return result;
}

这表明如果OnClickObserver类型的引用不为null,则performClick方法将调用onClick方法。

Flux 发布者

Flux类代表一个值流。这意味着 Flux 类型的实例可以发出值,订阅者可以接收它们。此类包含许多函数,可以分为两组:

  • 静态工厂允许我们从不同的来源创建 Flux 类型的新实例,例如回调或数组。

  • 允许我们处理发出值的操作符

以下示例代码展示了这是如何工作的:

fun fluxTest() {
    Flux.fromArray(arrayOf(1, 2, 3))
            .map { it * it }
            .subscribe { println(it) }
}

fromArray函数创建了一个新的 Flux 类型实例,该实例从传递的数组中逐个发出值。map方法允许我们修改上游的值,而subscribe方法需要传递一个 Observer 来接收结果值。

此示例的输出如下所示:

 1
 4
 9

Flux提供了许多可以用来处理发出值的操作符。以下示例代码演示了这一点:

Flux.fromArray(arrayOf(1, 2, 3))
        .filter { it % 2 == 1 }
        .map { it * it }
        .reduce { sum, item -> sum + item }
        .subscribe { println(it) }

.filter.map.reduce.subscribe操作符由 Flux 提供。我们将在稍后详细查看每一个。

从操作符的角度来看,流被分为上游下游。一个操作符上游获取一个值,修改它,并将结果传递到下游。以下图表展示了操作符是如何工作的:

map操作符的角度来看,filter函数发出的值属于上游,而reduce方法获取的项目属于下游

前一个示例的结果如下所示:

1
9

输出显示,在所有转换之后,Flux类的实例只发出两个数字。

过滤操作符

filter方法接受一个谓词,如果上游的值不满足谓词的条件,则不会将其传递到下游。

谓词是一个接受参数并返回布尔值的函数。

以下图表展示了在前一个示例中filter方法的工作原理:

在这个例子中,filter操作符仅用于接收奇数。

map操作符

map操作符接受一个 lambda,它为上游的每个值应用一个转换。map函数可以用来更改原始值的值,或者将一个实例从一种类型转换到另一种类型。

下面的图示展示了它是如何工作的:

图片

map函数接受另一个函数,该函数描述了上游元素应该如何转换。

flatMap操作符

flatMap操作符与map类似,但它是异步的。这意味着它应该返回一个可以在未来返回值的实例,例如FluxMono。以下示例代码展示了如何使用它:

Flux.fromArray(arrayOf(1, 2, 3))
        .flatMap { Mono.just(it).delayElement(Duration.ofSeconds(1)) }
        .subscribe { println(it) }

此示例的输出如下所示:

 1
 2
 3

MonoFlux类似,但它可以发出一个或零个元素。在这个例子中,我们使用了delayElement函数,这就是为什么每个元素都是通过一个订阅者以一秒的延迟接收到的。

下面的图示展示了它是如何工作的:

图片

这表明每个flatMap操作符都会异步地将每个值传递到下游,并延迟一秒钟。

reduce操作符

reduce函数接受BiFunction类型的实例,该实例包含apply函数,它接受两个值并返回一个。可以使用this操作符将上游的所有项目组合成一个单一值,如下所示:

图片

前面的图示显示上游包含两个值,reduce函数将它们的和传递到下游。

from静态方法

fromArray函数是Flux类提供的许多静态工厂方法之一。如果我们想创建自己的事件源,我们可以使用from函数。让我们创建一个当用户点击按钮时发出Unit对象的Flux类实例。

我们可以按如下方式实现这个案例:

Flux.from<Unit> { subscriber ->
    findViewById<Button>(R.id.button).setOnClickListener {
        subscriber.onNext(Unit)
    }
}.subscribe {
    Toast.makeText(this, "Clicked!", Toast.LENGTH_LONG).show()
}

前面的代码片段展示了如何将观察者包装成Flux类的实例。这个例子说明了如何使用from函数创建Flux类的新实例。

让我们运行一个应用程序并按下“THE OBSERVER PATTERN”按钮:

图片

前面的截图显示了示例的工作方式。当用户点击按钮时,onNext方法被调用,并且Observable发出一个值。我们传递给subscribe方法的 lambda 被调用,并显示一条消息。

取消

ActivityFragment类的实例具有由方法表示的生命周期,例如onCreateonDestroy。我们应该使用onDestroy方法清理所有资源,以避免内存泄漏。

subscribe方法返回一个Disposable类型的实例,如下所示:

public final Disposable subscribe(Consumer<? super T> consumer) {
   Objects.requireNonNull(consumer, "consumer");
   return subscribe(consumer, null, null);
}

Disposable接口包含两个方法,如下所示:

  • dispose 取消发布者

  • isDisposed 如果发布者已经被取消返回 true

以下示例代码展示了在调用 onDestroy 方法时如何取消发布者:

class ObserverActivity : AppCompatActivity() {

    private var disposable: Disposable? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_observer)
        disposable = Flux.from<Unit> { subscriber ->
            findViewById<Button>(R.id.button).setOnClickListener {
                subscriber.onNext(Unit)
            }
        }.subscribe {
            Toast.makeText(this, "Clicked!", Toast.LENGTH_LONG).show()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        disposable?.dispose()
    }
}

如您所见,onDestroy 方法调用 dispose 方法来取消对 Flux 类实例的订阅。

Mono 发布者

Mono 发布者与 Flux 的工作方式类似,但只能发出没有值或单个值。我们可以使用这个来向服务器发起请求并返回结果。

以下示例代码发起请求并接收 Comic 类的实例,加载 Bitmap 类的实例并显示检索到的图像:

Mono.fromDirect<Comic> { subscriber -> subscriber.onNext(loadComic()) }
        .map { comic -> comic.img }
        .flatMap { path -> Mono.fromDirect<Bitmap> { subscriber -> subscriber.onNext(loadBitmap(path)) } }
        .subscribeOn(Schedulers.single())
        .subscribe { bitmap ->
            Handler(Looper.getMainLooper()).post {
                findViewById<ImageView>(R.id.imageView).setImageBitmap(bitmap)
            }
        }

subscribeOn 方法用于指定长期任务的调度器。让我们按照以下方式运行此示例:

前面的代码片段检索 Comic 类的实例,将其转换为图像路径,加载图像,然后显示下载的图像。

阻塞和非阻塞

当我们与 Android 一起工作时,我们应该记住我们有一个负责用户界面的主线程。首先,在主线程中调用长期操作不是一个好主意,因为在这种情况下,用户界面会冻结。其次,当我们调用同步方法时,它会阻塞一个线程。我们的用户界面在主线程调用的函数返回结果之前不会响应。这就是为什么我们应该异步调用长期操作,响应式编程可以帮助我们做到这一点。

MonoFlux 类包含 publishOnsubscribeOn 方法,可以在调用操作符时切换线程。subscribeOn 方法用于指定产生发出值的调度器,而 publishOn 用于指定 observable 的下游的线程调度器。

调度器是对线程池的抽象。以下示例代码创建了一个使用主线程的自己的调度器:

val UIScheduler = Schedulers.fromExecutor { runnable ->     Handler(Looper.getMainLooper()).post(runnable) 
}

现在,我们可以以以下方式重写 Mono 发布者部分的一个示例:

Mono.fromDirect<Comic> { subscriber -> subscriber.onNext(loadComic()) }
        .map { comic -> comic.img }
        .flatMap { path -> Mono.fromDirect<Bitmap> { subscriber -> subscriber.onNext(loadBitmap(path)) } }
        .subscribeOn(Schedulers.single())
        .publishOn(UIScheduler)
        .subscribe { bitmap -> findViewById<ImageView>(R.id.imageView).setImageBitmap(bitmap) }

Schedulers 类的单个函数返回一个 Scheduler 类型的实例,该实例在底层创建和使用单个线程。subscribeOn 方法指定上游的所有操作符都必须使用由 single() 函数返回的调度器。

我们传递一个使用 主线程 的自己的调度器。因此,传递给 subscribe 方法的 lambda 表达式在 主线程 上执行。

以下图表显示了这是如何工作的:

图表显示 主线程 没有被阻塞,并且以后台并行运行。

RxJava

RxJava 是另一个流行的库,它实现了响应式编程的概念。它还提供了类型,如 Observable 或 Single,可以发出值。所有这些类也提供了静态工厂和操作符。

在本节中,我们将涵盖以下内容:

  • 如何使用 Flowable 类

  • 如何使用 Observable 类

  • 如何使用 Single 类

  • 如何使用 Maybe 类

  • 如何使用 Completable 类

Flowable

Flowable 类是在 RxJava 库的第二版中引入的。此类表示事件流,如 Reactor 中的 Flux。

当您从文件、数据库或网络读取数据时,应考虑使用 Flowable。以下示例代码展示了如何创建和使用 Flowable

Flowable.fromIterable(listOf(1, 2, 3))
        .subscribe { println(it) }

这显示了如何创建一个发出值的 Flowable 类的实例。

可观察性

Observable 类类似于 Flowable,但它可以抛出 MissingBackpressureException

背压是指可观察性产生的值比订阅者消耗得快的情形。在这种情况下,会抛出 MissingBackpressureException

一个示例用例如下:

Observable.fromIterable(listOf(1, 2, 3))
        .subscribe { println(it) }

前面的代码片段显示了如何创建一个发出值的 Observable 类的实例。

值得注意的是,Observable 的开销低于 Flowable。当您处理用户界面事件时,应考虑使用 Observable

有一些操作符可以帮助您处理背压,例如 debouncethrottle。让我们来看看每一个。

debounce 操作符

debounce 方法接受一个持续时间,并返回一个 Observable 类的实例,该实例仅在从上次发出值的时间点开始的时间段等于传递的时间时才发出值。以下图表解释了这是如何工作的:

图片

前面的图表显示了 debounce 方法如何减少事件。debounce 方法接受一个时间框架,并返回一个新的 Observable 类型的实例,该实例仅在当前时间框架内发出在此期间产生的最后一个值。

节流操作符

throttle 操作符返回一个 Observable 实例,该实例在上游的顺序时间窗口内只发出一个项目。节流是一系列方法,如 throttleFirstthrottleLast

以下图表显示了 throttleFirst 方法的工作方式:

图片

throttleLast 方法的工作方式如下:

图片

前面的图表显示,throttleFirstthrottleLast 方法可以用来减少发出的值。

Single

Single 类的工作方式与 Reactor 库中的 Mono 类似。这也可以用来向服务器发起请求。当源只返回一个项目时,我们应该考虑使用 Flowable

以下示例代码展示了如何使用 Single

Single.just(1).subscribe(Consumer<Int> { println(it) })

此代码片段包含一个发出单个值的 Single 类的实例。

Maybe

Maybe 类型的实例可以不发出值,或者发出单个值。以下示例代码展示了如何使用 Maybetest 方法:

Maybe.just(1)
        .map { item -> item + 1 }
        .filter { item -> item == 1 }
        .defaultIfEmpty(4)
        .test()
        .assertResult(4)

test 方法返回一个用于测试的 TestObservable 类的实例,其中包含 assertResult 等方法。Maybe 类的 defaultIfEmpty 方法允许我们指定一个默认值,当 Maybe 类的实例为空时可以发出。

Completable

Completable 类的实例根本不发出任何值。它可以用来通知用户任务完成。此外,当我们从数据库中删除项时,也可以使用它。

以下示例代码展示了从数据库中删除项的情况:

Completable.fromAction { Database.delete() }
        .test()
        .assertComplete()

test 方法返回 TestObservable 类的实例。

RxJava in Android

RxJava 是 Android 开发中非常流行的库,还有许多基于 RxJava 的其他库,例如 RxAndroid 和 RxBinding。

本节将涵盖以下主题:

  • The RxAndroid library

  • The RxBinding library

The RxAndroid library

RxAndroid 库提供了一个使用主线程的调度器。以下示例代码展示了如何使用此调度器:

Flowable.fromIterable(listOf(1, 2, 3))
        .subscribeOn(Schedulers.computation())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe { println(it) }

前面的代码片段展示了如何使用 observeOn 方法在主线程上处理发出的值。

The RxBinding library

RxBinding 库提供了一个响应式应用程序编程接口。让我们想象一下,我们想要观察 EditText 的输入并显示此文本在 TextView 中。

RxBinding 库为用户界面组件提供了扩展函数,例如 textChanges

fun TextView.textChanges(): InitialValueObservable<CharSequence> {
    return TextViewTextChangesObservable(this)
}

我们可以通过使用 textChanges 函数来实现我们的示例,如下所示:

class RxActivity : AppCompatActivity() {

    private val editText by lazy(LazyThreadSafetyMode.NONE) {
        findViewById<EditText>(R.id.editText)
    }

    private val textView by lazy(LazyThreadSafetyMode.NONE) {
        findViewById<TextView>(R.id.textView)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_rx)
        editText
                .textChanges()
                .subscribe { textView.text = it }

    }
}

在前面的代码片段中,我们调用了 textChanges 函数并订阅了一个检索到的订阅者。textChanges 方法返回一个发出输入文本的 Observable 类的实例。

结果如下所示,显示输入文本立即出现在屏幕上:

RxBinding 库还包含 clicks 扩展函数,如下所示:

fun View.clicks(): Observable<Unit> {
    return ViewClickObservable(this)
}

clicks 扩展函数返回一个 ViewClickObservable 类的实例。

此外,ViewClickObservable 看起来如下:

private class ViewClickObservable(
        private val view: View
) : Observable<Unit>() {

    override fun subscribeActual(observer: Observer<in Unit>) {
        if (!checkMainThread(observer)) {
            return
        }
        val observer = Observer(view, observer)
        observer.onSubscribe(observer)
        view.setOnClickListener(observer)
    }
  }

它使用 subscribeActual 方法将 Observer 类的实例传递给 View 类实例的 setOnClickListener

ViewClickObservable 类继承自 Observable 类并重写了 subscribeActual 方法。

最后,Observer 类看起来如下:

 private class Observer(
            private val view: View,
            private val observer: Observer<in Unit>
    ) : MainThreadDisposable(), OnClickObserver {

       override fun onClick(v: View) {
           if (!isDisposed) {
               observer.onNext(Unit)
           }
       }

       override fun onDispose() {
           view.setOnClickListerner(null)
       }
}

前面的代码片段在 onClick 方法被调用时调用 onNext 方法。

摘要

在本章中,我们探讨了响应式编程以及它是如何帮助我们处理异步事件的。我们还介绍了 React 和 RxJava 库,它们提供了遵循响应式编程概念的类,如 MonoFluxSingleObservable

响应式编程允许我们使用不同的线程调度器以多线程方式处理和转换事件。阻塞和非阻塞部分向我们展示了如何与线程调度器协同工作。你也了解到响应式编程基于观察者模式。

现代安卓应用程序处理许多不同的异步事件,例如用户交互和推送通知。了解响应式编程非常重要,因为它可以帮助我们通过异步处理更好地管理资源,使我们能够构建能够进行多任务处理的应用程序。

在下一章中,你将学习如何创建 Application 类。

问题

  • 什么是响应式编程?

  • 什么是 Mono 类?

  • 什么是 Observable 类?

  • 什么是调度器?

进一步阅读

第九章:创建应用程序

到目前为止,我们已经为您准备好成为一名专业的基于 Spring 的开发者。您已经学习了 Spring 是什么,以及其架构、组件、安全特性、数据库等功能。我们还向您展示了如何开发 Android 应用程序、处理 HTTP 请求以及使用数据库。

正如您所知,我们使用 Kotlin 语言开发了所有示例项目,而如今,这种语言因其简洁性和互操作性而在开发者中非常受欢迎。在本章中,我们将实现前几章的所有功能,以开发一个具有服务器和客户端的项目。

本章涵盖了以下主题:

  • 项目想法

  • 创建设计

  • 服务器端:

    • 开发数据库模型

    • 创建项目和 Maven 依赖项

    • 创建实体、仓库和控制器

    • 实现安全性

    • 修改后的 application.properties

  • 客户端:

    • 创建模型

    • 创建 HTTP 请求

    • 创建 API 服务

    • 修改活动

    • 获取 REST API

    • 创建适配器和 XML 布局

    • 检查输出

技术要求

你将需要几乎所有来自前几章的依赖项,例如安全、MySQL、JPA、Hibernate 和 JDBC。

本章的示例源代码可在 GitHub 上的以下链接找到:github.com/PacktPublishing/Learn-Spring-for-Android-Application-Development/tree/master/Chapter09

您将找到两个项目——social_network是服务器端,它是在 Spring 框架的帮助下开发的,而ClientSide是客户端,它是为 Android 平台开发的。

项目想法

项目想法是最重要的部分。您需要非常仔细地生成这个想法,并必须识别您项目背后的事实。您需要记住这个项目如何在市场上有效,用户将如何接受您的项目,为什么他们会使用它,为什么他们应该选择您的应用程序而不是其他应用程序,哪些功能将使其与其他现有类似项目不同,等等。在生成想法后,您需要在脑海中构思一个草稿,然后将其写在纸上,设计项目的流程,然后开发项目的代码。最后,您需要测试项目以确保其流畅性,检查是否存在错误或漏洞,并为其上市做准备。

在本章中,我们将创建一个类似社交网络的小型项目。我们将将其命名为Packt Network。该项目将有两个部分:一个是服务器,另一个是客户端,这两部分都将使用 Kotlin 编写。首先,我们创建一个 Spring 项目,我们将在这里构建我们的服务器和 REST API。数据将存储在 MySQL 数据库中,我们将使用 JDBC、JPA 和 Hibernate 来处理数据库。这些数据将通过 Spring Security 的基本身份验证得到保护。

在我们的第二个项目中,我们将创建一个 Android 应用程序,并处理服务器创建的 REST API。我们将使用 Retrofit 来处理 REST API 和网络。然后我们将创建一个注册和登录页面,使用用户名和密码创建和登录用户。之后,用户可以发布状态并查看其他所有用户的状态列表。也可以在状态中发布评论。

现在我们将开始设计和开发我们的服务器端项目,使用 Spring 框架。

服务器端

在服务器端,我们使用 Spring 框架。我们将使用 MySQL 数据库处理所有数据,并使用基本认证保护资源。

首先,我们将设计项目的后端逻辑。然后我们将规划 REST API。我们将使用 MySQL Workbench 创建数据模型。然后我们将使用 start.spring.io 创建项目。然后我们将使用 JPA 和 Hibernate 创建数据库实体,并检查 REST API 是否工作。为此,我们将使用名为 Insomnia 的 HTTP 客户端软件工具。然后我们将使用 Spring Security 实现基本认证以保护我们的资源。最后,我们将给你一个任务来完成,即升级项目,并成为 GitHub 上这个项目的贡献者。

创建设计

正如我们之前提到的,这个项目将类似于一个社交媒体平台;用户可以发布他们的状态,其他人可以在时间轴中看到它们,并可以点赞、添加评论等等。对于这个项目,服务器端不会有 UI。我们将创建一个后端服务器。为了创建这个服务器,我们需要创建一个客户端应用程序可以使用的 REST API。为此,我们需要基于我们的 REST API 创建一个数据库。

首先,我们将数据库表名、HTTP 函数请求和 URL 路径分开。

将有四个表:

让我们逐一查看它们:

  • 其中一个是用于用户的。所有用户的信息都将存储在一个名为 Profile 的表中。

  • 还将有一个名为 Post 的表,其中将存储所有用户的发布状态。

  • 另一个名为 Comment 的表将存储所有发布状态的评论。

  • 另一个名为 LikeObj 的表将存储所有发布状态的点赞,但我们不会为评论提供此功能。

现在我们将使用 HTTP 函数请求创建 REST API 的 URL 路径,并且所有输出都将设计为 JSON。我们使用 JSON,因为它非常容易为所有开发者处理和理解。

关于 Profile 表,以下是 HTTP 请求的 URL 路径:

  • POST http://localhost:8080/user/new: 这个请求将创建一个包含用户在个人资料中发布的所有信息的用户资料

  • GET http://localhost:8080/user/{id}: 这个请求将获取给定 id 持有者的详细信息

  • PUT http://localhost:8080/user/{id}: 这个请求将更新给定id持有者的用户详细信息

  • DELETE http://localhost:8080/user/{id}: 这个请求将删除给定id持有者的用户详细信息,包括此用户的所有帖子、评论和点赞

关于Post表,以下是 HTTP 请求的 URL 路径:

  • POST http://localhost:8080/post/{id}/new: 这个请求将从id持有者创建一个帖子

  • GET http://localhost:8080/posts: 这个请求将获取所有帖子的详细信息

  • GET http://localhost:8080/post/{id}: 这个请求将获取给定id持有者的帖子详细信息

  • DELETE http://localhost:8080/post/{id}: 这个请求将删除给定id持有者的帖子详细信息,包括所有评论

关于Comment表,以下是 HTTP 请求的 URL 路径:

  • POST http://localhost:8080/comment/{post_id}: 这个请求将在post_id持有者上创建一个评论

  • DELETE http://localhost:8080/comment/{post_id}: 这个请求将删除给定post_id持有者的评论

关于LikeObj表,以下是 HTTP 请求的 URL 路径:

  • POST http://localhost:8080/like/new: 这个请求将点赞post_id持有者的帖子

  • DELETE http://localhost:8080/like/new: 这个请求将取消点赞post_id持有者的帖子

开发数据库模型

我们将使用 JPA,最明显的点之一是不建议创建数据库,因为众所周知,JPA 将自动使用项目的实体类创建数据库表和字段。但仍然,我们需要创建一个演示数据库并绘制一个 EER 图。您可以在纸上创建 EER,或者您可以使用 MySQL Workbench 在数字上创建一个。这里,我们将使用 MySQL Workbench,它有一个免费版本。这是开发数据库或为数据库创建模型的最佳工具之一:

  1. 您需要从这里下载此软件,如果您还没有的话。然后安装并运行它。正如我们之前提到的,我们有一些默认值:
Host -- localhost // our hosting URL
Port -- 3306 // our hosting port
Username -- root // username of the MySQL
Password -- 12345678 // password of the MySQL
  1. 打开此应用程序并选择模型选项,如下面的截图所示:

  1. 点击加号(+)为我们的应用程序创建一个新的模型。在新窗口中,您将找到创建模型所需的所有必要功能。将此模型保存为my_app

  1. 创建一个名为Profile的表。列将是id(主键)usernamepasswordemailfirst_namelast_nameacc_created_timecontact_numberdobcitycountry

  2. 创建一个名为Post的表。列将是id(主键)text

  3. 创建另一个名为Comment的表。列将是id(主键)text

  4. 最后,创建一个名为Like的表。列将是id(主键)

但是,表之间有一些关系:

  • ProfilePost 之间:Post 存在多对一关系,因为一个用户可以发布多条状态,而每条状态只有一个用户。

  • ProfileComment 之间:Comment 存在多对一关系,因为一个用户可以发表多条评论,而每条评论只有一个用户。

  • ProfileLike 之间:Like 存在多对一关系,因为一个用户可以点赞多个帖子,而每个点赞只有一个用户。

  • PostComment 之间:Post 存在一对多关系,因为一篇文章可能有多个评论,但评论只针对一篇文章。

  • PostLike 之间:Post 存在一对多关系,因为一篇文章可能有多个点赞,但每个点赞只针对一篇文章:

因此,在所有关系确定之后,我们可以看到数据库的表名,如下面的截图所示:

最后,你可以通过点击 EER 图标创建 EER 图,如下面的截图所示:

这是我们的项目的 EER 图模型。你可能还会发现两个额外的表,名为 post_likespost_comments。这些表是通过 JPA 和 Hibernate 生成的。我们将在稍后讨论这个问题。

因此,我们的数据库建模已经完成。现在你可以将其导出为 SQL 并为项目创建数据库。但我们建议你不要这样做,因为我们需要进行一些修改。

现在创建项目。

创建项目

要创建一个项目,请访问 start.spring.io 并创建一个基于 Kotlin 的项目。以下是项目的依赖项:

  • Web

  • JDBC

  • MySQL

  • DevTools

  • JPA

  • H2

你可以在 pom.xml 文件中找到这些依赖项。在那里你可以更新、添加或删除依赖项。

要启用 JPA 审计,你需要在 SocialNetworkApplication.kt 类上添加 @EnableJpaAuditing 注解。这将启用 JPA 功能的使用。

这是这个类的代码:

@SpringBootApplication
@EnableJpaAuditing class SocialNetworkApplication

fun main(args: Array<String>) {
   runApplication<SocialNetworkApplication>(*args)
}

创建实体

首先,我们需要创建四个数据库表细节。这四个实体是 ProfilePostCommentLikeObj。在接下来的章节中,你将学习如何创建实体类。

创建 Profile 实体

使用 @Entity 注解创建一个名为 Profile.ktProfile 实体,将其转换为实体类。以下是这个模型类的代码(完整代码可以在提供的 GitHub 链接中找到):

@Entity
class Profile : Serializable {

    constructor(id: Long) {
        this.id = id
    }

    constructor(name: String) {
        this.username = name
    }
    -----
    -----
 @JsonProperty("contactNumber")
 var contactNumber: String? = null

 @JsonProperty("dob")
 var dOB: Date? = null

 @JsonProperty("city")
 var city: String? = null

 @JsonProperty("country")
 var country: String? = null
}

在这个类中,我们有 11 个元素,包含所有用户详情。我们有四个构造函数,可以根据我们的任务使用这个模型。以下是构造函数:

constructor(id: Long) {
  ----
  ----
}

constructor(name: String) {
  ----
  ----
}

constructor(id: Long, name: String, password: String) {
  ----
  ----
}

constructor(username: String, password: String, email: String, accCreatedTime: Instant,
 firstName: String?, lastName: String?, contactNumber: String?, dOB: Date?,
 city: String?, country: String?) {
  ----
  ----
}

现在我们来讨论这个类中使用的注解:

@Id
@GeneratedValue
var id: Long? = 0

根据前面的代码,我们在 id 上使用了 @Id 注解,这意味着 idProfile 实体的主键。@GeneratedValue 注解表示它会增加 id 的值。

这是 password 对象的代码片段:

@JsonIgnore
@JsonProperty("password")
var password: String = ""

根据这段代码,@JsonIgnore 使用变量或函数。如果你使用它,那么请求的 JSON 不会显示这个变量。在这里,我们用它来处理 password,这意味着没有人可以获取密码。

@JsonProperty 定义了在 JSON 的序列化和反序列化过程中,它改变了其元素逻辑属性的可见性。

创建一个帖子实体

创建一个名为 Post.kt 的帖子实体,并使用 @Entity 注解将其转换为实体类。以下是这个模型类的代码:

@Entity
class Post(text: String, postedBy: Profile) : Serializable {

    @Id
    @GeneratedValue
    var id: Long? = 0

    var text: String? = text

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "profile_id")
    @JsonIgnoreProperties("username","password", "email","accCreatedTime","firstName","lastName",
            "contactNumber","dob","city","country")
    var postedBy: Profile? = postedBy

    @JsonIgnore
    @JsonProperty("postCreatedTime")
    var postCreatedTime: Instant? = Instant.now()

    @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY, orphanRemoval=true)
    val comments = mutableListOf<Comment>()

    @OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true)
    var likes: List<LikeObj>? = mutableListOf<Comment>()
}

这里有两个元素和一个构造函数。以下是构造函数:

@Entity
class Post(text: String, postedBy: Profile) : Serializable {
   -----
   -----
}

现在是时候讨论一些在这个类中使用的新注解了:

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id")
@JsonIgnoreProperties("username","password", "email","accCreatedTime","firstName","lastName",
            "contactNumber","dob","city","country")
    var postedBy: Profile? = postedBy

@ManyToOneProfile 变量上表示这将指示哪个用户发布了那个特定的状态。

@JoinColumn 表示其访问元素 Profile 通过 profile_id 与外键连接。

@JsonIgnoreProperties(......) 注解在反序列化过程中忽略 JSON 属性。在这个项目中,当你获取帖子的 JSON 时,在 profile 属性中你只会找到 id。以下是一个简单的 JSON 示例:

图片

你可以看到 "id":0,这是帖子的id

现在创建一个 Comment 的可变列表,并用 @OneToMany 注解它,如下所示:

 @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY, orphanRemoval=true)
    val comments = mutableListOf<Comment>()

@OneToMany(....) 表示一个帖子可以有多个评论和点赞。

cascade = [CascadeType.ALL] 属性是 Hibernate 的一个特性。这意味着你可以应用所有主要级联类型。

fetch = FetchType.LAZY 表示在第一次访问时它会懒加载数据。

orphanRemoval=true 表示如果帖子被删除,那么这个帖子上的所有评论和点赞也会自动删除。

创建评论实体

创建一个名为 Comment.ktComment 实体,并使用 @Entity 注解将其转换为实体类。以下是这个模型类的代码:

@Entity
class Comment(text: String, postedBy: Profile) : Serializable {

    @Id
    @GeneratedValue
    var id: Long? = 0

    var text: String? = text

    @JsonIgnore
    @JsonProperty("accCreatedTime")
    var accCreatedTime: Instant? = Instant.now()

    @ManyToOne
    @JoinColumn(name = "profile_id")
        @JsonIgnoreProperties("username","password","email","accCreatedTime","firstName","lastName"       , "contactNumber","dob","city","country")
    var postedBy: Profile? = postedBy
}

这里有三个元素和一个构造函数。以下是构造函数:

@Entity
class Comment(text: String, postedBy: Profile) : Serializable {
   -----
   -----
}

创建点赞实体

创建一个名为 LikeObj.kt 的点赞实体,并使用 @Entity 注解将其转换为实体类。以下是这个模型类的代码:

@Entity
class LikeObj(mProfile: Profile) : Serializable {

    @Id
    @GeneratedValue
    var id: Long? = 0

    @ManyToOne
    @JoinColumn(name = "profile_id")
    @JsonIgnoreProperties("username","password","email","accCreatedTime","firstName","lastName",
            "contactNumber","dob","city","country")
    var profile: Profile? = mProfile
}

这里有一个元素和一个构造函数。以下是构造函数:

@Entity
class LikeObj(profile: Profile) : Serializable {
   -----
   -----
}

创建仓库

创建一个名为 ProfileRepository.kt 的用户资料仓库,并实现具有所有必要的 CRUD 请求方法的 JpaRepository 仓库以获取数据库。以下是这个类的代码:

@Repository
interface ProfileRepository : JpaRepository<Profile, Long>

现在创建一个名为 PostRepository.kt 的帖子仓库,并实现具有所有必要的 CRUD 请求方法的 JpaRepository 仓库以获取数据库。以下是这个类的代码:

@Repository
interface PostRepository : JpaRepository<Post, Long>

然后创建一个名为 CommentRepository.kt 的评论仓库,并实现具有所有必要的 CRUD 请求方法的 JpaRepository<> 仓库以获取数据库。以下是这个类的代码:

@Repository
interface CommentRepository : JpaRepository<Comment, Long>

最后,创建一个名为 LikeRepository.ktlike 模型存储库,并实现具有所有必要 CRUD 请求方法的 JpaRepository<> 存储库以获取数据库。以下是该类的代码:

@Repository
interface LikeRepository : JpaRepository<LikeObj, Long>

要删除与已删除帖子相关的所有数据,我们需要为名为 DeletePCLRepository.ktprofile 创建一个存储库,并实现一个名为 DeletePCLByIDInterface.kt 的接口,该接口有一个函数,用于删除与已删除用户相关的所有数据。以下是该接口的代码:

interface DeletePCLByIDInterface {
    fun deleteAllUsersInfoByUserID(userID: Long): Any
}

这里是 DeletePCLRepository.kt 类的代码:

@Repository
class DeletePCLRepository : DeletePCLByIDInterface {

    @Autowired
    private lateinit var jdbcTemplate: JdbcTemplate

    override fun deleteAllUsersInfoByUserID(userID: Long): Any {

        val deletePosts = "DELETE FROM post, comment WHERE profile_id = ?;"
        val deleteComments = "DELETE FROM comment WHERE profile_id = ?"
        val deleteLikes = "DELETE FROM like_obj WHERE profile_id = ?"

        jdbcTemplate.update(deletePosts, userID)
        jdbcTemplate.update(deleteComments, userID)
        jdbcTemplate.update(deleteLikes, userID)

        return "DONE"
    }
}

要检查注册用户,创建一个名为 UserExistRepository.kt 的存储库,并实现一个名为 UserExistInterface.kt 的接口,该接口有两个函数。

这里是接口的代码:

interface UserExistInterface{
    fun isUserExist(name: String): Boolean
}

在这个接口中,isUserExist(username: String) 将搜索数据库的 Profile 表并基于用户的存在返回一个 Boolean

这里是 UserExistRepository.kt 类的代码:

@Repository
class UserExistRepository: UserExistInterface {
    @Autowired
    private lateinit var jdbcTemplate: JdbcTemplate

    override fun isUserExist(name: String): Boolean {
        val sql = "SELECT count(*) FROM PROFILE WHERE username = ?"
        val count = jdbcTemplate.queryForObject(sql, Int::class.java, name)
        return count != 0
    }
}

在这个类中,我们添加了 @Autowired 注解来自动装配 JdbcTemplate 以利用 JDBC 数据库。我们 重写issue exist(name: String) 函数。

"SELECT count(*) FROM PROFILE WHERE username = ?" 是一个用于从数据库的 Profile 表中搜索现有用户的 SQL 查询。如果存在用户,则它将返回 true

创建控制器

现在,创建一个名为 AppController.kt 的控制器类,并使用 @RestController 注解将其转换为控制器类:

@RestController class AppController {
    -----
    -----
}

现在按照以下代码自动装配存储库。

@Autowired
private lateinit var profileRepository: ProfileRepository

@Autowired
private lateinit var userExist: UserExistRepository

@Autowired
private lateinit var postRepository: PostRepository

@Autowired
private lateinit var commentRepository: CommentRepository

@Autowired
private lateinit var likeRepository: LikeRepository

@Autowired
private lateinit var deletePCLRepository : DeletePCLRepository

然后创建 HTTP 函数请求。我们在这里不讨论这个问题,因为我们已经在 第四章,Spring Modules for Android 中描述了 HTTP 请求的使用。

创建个人资料的 HTTP 请求

现在为个人资料创建 HTTP 请求函数。

这里是创建个人资料 POST 请求的函数:

// New Profile registration
@PostMapping("/profile/new")
fun registerUser(@RequestBody profile: Profile): Any {
    if (!userExist.isUserExist(profile.username)) {
        profile.password = passwordEncoder.encode(profile.password)
        profileRepository.save(profile)
        return profile
    }
    return "{\"duplicate\": \"${profile.username} is taken. Try another\"}"
}

这里是创建个人资料 GET 请求的函数:

// Get Profile by ID
@GetMapping("/profile/{id}")
fun getUserById(@PathVariable("id") id: Long): Any {
    return profileRepository.findById(id)
}

这里是创建个人资料 PUT 请求的函数:

//     Update Profile by ID
@PutMapping("/profile/{id}")
fun updateUserById(@PathVariable("id") id: Long, @RequestBody mUser: Profile): Any {
    val profile = profileRepository.getOne(id)
    if (mUser.firstName != null) profile.firstName = mUser.firstName
    if (mUser.lastName != null) profile.lastName = mUser.lastName
    if (mUser.contactNumber != null) profile.contactNumber = mUser.contactNumber
    if (mUser.city != null) profile.city = mUser.city
    if (mUser.country != null) profile.country = mUser.country
    return profileRepository.save(profile)
}

这里是创建个人资料 DELETE 请求的函数:

// Delete Profile by ID
@DeleteMapping("/profile/{userId}")
fun deleteUserById(@PathVariable("userId") userId: Long): Any {
    deletePCLRepository.deleteAllUsersInfoByUserID(userId)
    return profileRepository.deleteById(userId)
}

创建帖子的 HTTP 请求

现在为 Post 创建 HTTP 请求函数。

这里是创建帖子 POST 请求的函数:

// Post status by Profile ID
@PostMapping("/post/{profile_id}/new")
fun submitPost(@PathVariable("profile_id") profile_id: Long, @RequestParam text: String): Any {
        val mPost = Post(text, Profile(profile_id))
        postRepository.save(mPost)

        return mPost
    }

这里是创建帖子 GET 请求以获取所有帖子的函数:

// Get all posted status
@GetMapping("/posts")
fun getPostList(): Any {
    return postRepository.findAll()
}

这里是创建帖子 GET 请求以获取单个帖子的函数:

// Get all posted status by Profile ID
@GetMapping("/post/{id}")
fun getPostById(@PathVariable("id") id: Long): Any {
    return postRepository.findById(id)
}

这里是创建帖子 PUT 请求以更新单个帖子的函数:

// Update all posted status by Profile ID
 @PutMapping("/post/{profile_id}")
    fun updatePostById(@PathVariable("profile_id") id: Long, @RequestParam text: String): Any {
        val modifiedPost = postRepository.getOne(id)
        modifiedPost.text = text
        return postRepository.save(modifiedPost)
    }

这里是创建帖子 DELETE 请求的函数:

// Delete a posted status by Profile ID
@DeleteMapping("/post/{id}")
fun deletePostByUserId(@PathVariable("id") id: Long): Any {
    return postRepository.deleteById(id)
}

创建评论的 HTTP 请求

现在创建 Comment 的 HTTP 请求函数。

这里是创建评论 POST 请求的函数:

// Post comment in a post by Profile ID and Post ID
    @PostMapping("/comment/{post_id}")
    fun postCommentByPostId(@PathVariable("post_id") postId: Long, @RequestParam id: Long, @RequestParam commentText: String): Any {
        val optionalPost: Optional<Post> = postRepository.findById(postId)
        return if (optionalPost.isPresent) {
            val myComment = Comment(commentText, Profile(id))
            val post = optionalPost.get()
            post.comments.add(myComment)
            postRepository.save(post)
            return post
        } else {
            "There is no post.."
        }
    }

首先,我们需要通过查找现有帖子来初始化一个 optionalPost 对象。然后,如果帖子存在,我们创建一个名为 myCommentComment 模型,然后添加可变列表的 Comment,然后使用 postRepository 保存帖子。

这里是创建评论 GET 请求的函数:

// get comment List of a post
@GetMapping("/comment/{id}")
fun getCommentListByPostId(@PathVariable("id") id: Long): Any {
    return commentRepository.findById(id)
}

这里是创建评论 PUT 请求的函数:

// get comment List of a post
@GetMapping("/comment/{id}")
fun getCommentListByPostId(@PathVariable("id") id: Long, @RequestParam text: String): Any {
    val modifiedComment = commentRepository.getOne(id)
    modifiedComment.text = text
    return commentRepository.save(modifiedComment)
}

这里是创建评论 DELETE 请求的函数:

// delete comment List of a status
@DeleteMapping("/comment/{id}")
fun deleteCommentByPostId(@PathVariable("id") id: Long): Any {
    return commentRepository.findById(id)
}

实现安全

我们正在实现基本认证安全。它将类似于我们在第五章,使用 Spring Security 保护应用程序中讨论的内容。但我们在那里使用了 inMemoryAuthentication(),而在这里我们将从数据库中获取用户名和密码,并使用 UserDetailsService 为项目实现它们:

  1. 创建一个名为 CustomUserDetailsService.kt 的服务类。

  2. 实现 UserDetailsService 并用 @Service 注解,使其成为一个服务类。以下是这个服务类的代码:

@Service
class CustomUserDetailsService: UserDetailsService {

 @Autowired
 private lateinit var userByNameRepository: UserByNameRepository

 @Throws(UsernameNotFoundException::class)
 override fun loadUserByUsername(username: String): User {
 val profile = userByNameRepository.getUserByName(username)

 return org.springframework.security.core.userdetails.User(username, profile.password,
 AuthorityUtils.createAuthorityList("USER"))
 }
}
  1. 在这里,我们自动装配了 UserByNameRepository.kt 存储库并覆盖了 loadUserByUsername(username: String)。我们将从存储库中获取 usernamepassword 并与客户端提供的 usernamepassword 进行匹配。以下是 UserByNameRepository.kt 的代码:
@Repository
class UserByNameRepository: UserByNameInterface {
 @Autowired
 private lateinit var jdbcTemplate: JdbcTemplate

 override fun getUserByName(username: String): Profile {
 val sql = "SELECT * FROM PROFILE WHERE username = ?"
 val profile = jdbcTemplate.queryForObject(sql, UserRowMapper(), username)

 return profile!!
 }

 override fun getUserByNamePassword(username: String, password: String): Boolean {
 val sql = "SELECT * FROM PROFILE WHERE username = ?, password = ?"
 val profile = jdbcTemplate.queryForObject(sql, UserRowMapper(), username, password)
 return profile != null
 }
}

interface UserByNameInterface {
 fun getUserByName(username: String): Profile
 fun getUserByNamePassword(username: String, password: String): Boolean
}
  1. 现在创建名为 UserRowMapper.kt 的用户 RowMapper 类的代码,以获取用户详细信息。以下是这个类中的一段代码:
class UserRowMapper : RowMapper<Profile> {

    @Throws(SQLException::class)
    override fun mapRow(row: ResultSet, rowNumber: Int): Profile? {
        val profile = Profile(row.getLong("id"),
                row.getString("username"),
                row.getString("password"))
        return profile
    }
}
  1. 让我们创建一个名为 SecurityConfigurer.ktWebSecurityConfigurerAdapter 类,并用 @Configuration@EnableWebSecurity 注解它,以创建一个配置文件并启用网络安全。以下是 SecurityConfigurer.kt 类的代码:
@Configuration
@EnableWebSecurity
class SecurityConfigurer : WebSecurityConfigurerAdapter() {

 @Autowired
 private lateinit var authEntryPoint: AuthenticationEntryPoint

 @Autowired
 private lateinit var customUserDetailsService: CustomUserDetailsService

 @Throws(Exception::class)
 override fun configure(http: HttpSecurity) {
 http.csrf().disable().authorizeRequests()
 .antMatchers("/profile/new").permitAll()
 .anyRequest()
 .authenticated()
 .and()
 .formLogin()
 .and()
 .httpBasic()
 .authenticationEntryPoint(authEntryPoint)
 }

 @Autowired
 @Throws(Exception::class)
 fun configureGlobal(auth: AuthenticationManagerBuilder) {
 auth
 .userDetailsService(customUserDetailsService)
 .passwordEncoder(getPasswordEncoder())
 }

 @Bean
 fun getPasswordEncoder(): PasswordEncoder {
 return object : PasswordEncoder {
 override fun encode(charSequence: CharSequence): String {
 return charSequence.toString()
 }

 override fun matches(charSequence: CharSequence, s: String): Boolean {
 return true
 }
 }
 }
}

在前面的代码中,我们做了以下操作:

  • 要使用此注册 URL 路径 "/profile/new",任何用户都可以访问。它不需要 usernamepassword

  • 我们使用 PasswordEncoder 对密码进行编码。

  • 我们自动装配了 configureGlobal(auth: AuthenticationManagerBuilder) 并通过 auth.userDetailsService(customUserDetailsService) 传递 CustomUserDetailsService 来检查和匹配用户名。

修改后的 application.properties

application.properties 文件用于将数据库与应用程序连接,并定义数据库将如何行为。以下是 application.properties 的代码:

# ===============================
# DATABASE
# ===============================

spring.datasource.url=jdbc:mysql://localhost:3306/my_app_schema?useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=12345678

# ===============================
# JPA / HIBERNATE
# ===============================
spring.jpa.show-sql=true

# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update

## Hibernate Properties
# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect

在这里,我们使用了名为 my_app 的数据库,模式为 my_app_schema。我们通过 useSSL=false 禁用 useSSL,并且为了检索公钥,我们使用 allowPublicKeyRetrieval=true

这里,我们使用 spring.jpa.hibernate.ddl-auto = update,这意味着在重启服务器后数据不会丢失。

客户端

在创建我们的后端之后,我们需要创建一个基于客户端的应用程序来利用服务器。在这一部分,我们将创建一个 Android 应用程序作为基于客户端的前端应用程序。为了创建应用程序,我们需要在开始编码之前进行设计。我们将创建一个 Android 应用程序,并使用 Retrofit 处理 HTTP 请求。

首先,我们将设计应用程序的工作流程。

创建设计

为了设计我们的应用程序,我们必须牢记项目的内容以及后端是如何设计的。正如我们所知,这是一个类似微型社交网络的程序。因此,我们必须创建一些与服务器模型对象完全相同的模型对象。在应用程序的工作流程中,我们将有一些布局来表示我们的应用程序。

工作流程如图所示:

图片

根据以下图表,以下是工作流程的简要说明:

  • 登录页面:如果您有注册账户,您可以输入用户名和密码进入应用程序的主页。或者,如果您是新手,您需要转到注册页面并注册一个账户。

  • 注册页面:这是用于注册账户的。

  • 主页活动:这是您应用程序的主要部分。

  • 个人资料:您可以在这里查看您的详细信息。

  • 状态详情:您可以查看您点击的任何帖子的详细信息。

到目前为止,该项目基于这些布局。现在我们需要创建一个 Android 应用程序。

创建项目

要创建一个新项目,请转到 Android Studio 并点击新建项目。这次,选择 Android for Mobile**,然后选择 Basic Activity,如图所示:

图片

实现依赖项

在构建项目后,在 build.gradle (Module:app)dependencies{} 块中添加以下依赖项:这些是用于 Material Design、Retrofit 和 RxJava 的:

// Design
implementation 'com.android.support:design:28.0.0'
implementation 'com.android.support:recyclerview-v7:28.0.0'
implementation 'com.android.support:cardview-v7:28.0.0'

// Retrofit
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version"
implementation "com.squareup.retrofit2:retrofit-converters:$retrofit_version"
implementation "com.squareup.retrofit2:retrofit-adapters:$retrofit_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp3_version"
implementation "com.google.code.gson:gson:$gson_version"

// Rx
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
implementation 'io.reactivex.rxjava2:rxjava:2.2.0'

创建主页活动

创建项目后,你会找到 MainActivity,但在这里我们将其重命名为 HomeActivity.kt,布局名称为 activity_home

现在转到活动,以下是此类的默认代码:

class HomeActivity : AppCompatActivity() {

 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main2)
        setSupportActionBar(toolbar)

        fab.setOnClickListener { view ->
            Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                .setAction("Action", null).show()
        }
    }
}

修改布局

首先,创建一个名为 home_content.xml 的布局,添加 FrameLayout 并添加一个 id 名称。以下是此 XML 文件的代码(您可以在 GitHub 上查看完整版本):

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.MainActivity">
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 xmlns:tools="http://schemas.android.com/tools"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 tools:context=".ui.MainActivity">

      ----
----

        <android.support.v7.widget.RecyclerView
                android:id="@+id/displayList"
                android:layout_width="0dp"
                android:layout_height="0dp"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                tools:listitem="@layout/post_item"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintHorizontal_bias="0.0" android:layout_marginTop="8dp"
                app:layout_constraintTop_toBottomOf="@+id/appBarLayout"/>

    </android.support.constraint.ConstraintLayout>

    <android.support.design.widget.FloatingActionButton
            android:id="@+id/fabMain"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:layout_margin="@dimen/fab_margin"
            app:srcCompat="@android:drawable/ic_dialog_email"/>
</android.support.design.widget.CoordinatorLayout>

创建模型

为了创建模型,我们需要保持与后端相同的模型项。但我们将包括 Gson 注解,@SerializedName@SerializedName 注解的值用于对象的序列化和反序列化。在这里,@SerializedName("username") 表示这是 JSON 中的 Username 名称。尽管我们实现了 Gson,但你可以将这些模型类作为 API 的响应来调用。这意味着当这个应用程序请求服务器并获取内容时,这些内容将通过这些模型类返回。

创建个人资料模型

创建一个名为 Profile.ktProfile 数据类,以下是一个示例代码:

data class Profile(
    @SerializedName("id") var userID: String,
    @SerializedName("username") var username: String,
    @SerializedName("password") var password: String,
    @SerializedName("email") var email: String,
    @SerializedName("accCreatedTime") var accCreatedTime: String,
    @SerializedName("firstName") var firstName: String,
    @SerializedName("lastName") var lastName: String,
    @SerializedName("contactNumber") var contactNumber: String,
    @SerializedName("country") var country: String
    )

创建帖子模型

创建一个名为 Post.ktPost 数据类,以下是一个示例代码:

data class Post(
    @SerializedName("id") var postId: Long?,
    @SerializedName("text") var text: String?,
    @SerializedName("postedBy") var profile: Profile?,
    @SerializedName("accCreatedTime") var accCreatedTime: String?,
    @SerializedName("comments") var comment: ArrayList<Comment>?,
    @SerializedName("likes") var likes: ArrayList<Like>?
)

创建评论模型

创建一个名为 Comment.kt 的注释数据类,以下是一个示例代码:

data class Comment (
    @SerializedName("id") var comment: Long?,
    @SerializedName("text") var text: String?,
    @SerializedName("postedBy") var profile: Profile?,
    @SerializedName("accCreatedTime") var accCreatedTime: String?
    )

创建服务

这是最重要的部分。这将向服务器发送 GET 请求以获取数据。首先,我们将创建模型类的服务。我们将使用 Retrofit 注解创建 HTTP 请求函数,这些函数在 第四章 中称为 HTTP 请求函数 的部分中进行了解释,Android 的 Spring 模块

创建个人资料服务

根据我们的服务器,我们有四个针对个人资料的 HTTP 请求。因此,我们将使用 Retrofit 注解创建三个 HTTP 请求。现在创建一个名为 ProfileService.kt 的接口,以下是代码:

interface ProfileService {

    // New Profile registration
    @Headers("Content-Type: application/json")
    @POST("/profile/new")
    fun registerProfile(@Body profile: Profile): Observable<Profile>

    @Headers("Content-Type: application/json")
    @GET("/profile/login")
    fun loginProfile(@Query("username") username: String, @Query("password") password: String): Observable<Profile>

    // Get All Profiles
    @Headers("Content-Type: application/json")
    @GET("/profiles")
    fun getUserList(): Observable<List<Profile>>

   // Get Profile by ID
    @GET("/profile/{userId}")
    fun getUserById(@Path("userId") userId: Long): Observable<Profile>
}

根据前面的代码,以下是函数的简要细节:

  • registerProfile(@Body profile: Profile) 注册一个新的个人资料。你需要传递一个项目对象。

  • getUserList() 获取所有个人资料。

  • getUserById(@Query("userId") userId: Long) 获取一个个人资料。你需要传递用户 ID。

创建帖子服务

根据我们的服务器,我们有三个针对个人资料的 HTTP 请求。因此,我们将使用 Retrofit 注解创建三个 HTTP 请求。现在创建一个名为 ProfileService.kt 的接口,以下是代码:

interface PostService {
    @Headers("Content-Type: application/json")
    @POST("/post/{profile_id}/new")
    fun submitNewPost(@Path("profile_id") id: Long, @Query("text") text: String): Observable<List<Post>>

    // Get all posted status
    @Headers("Content-Type: application/json")
    @GET("/posts")
    fun getPostList(): Single<List<Post>>

    // Get all posted status by Profile ID
    @Headers("Content-Type: application/json")
    @GET("/post/{id}")
    fun getPostById(@Path("id") id: Long): Observable<Post>

}

根据前面的代码,以下是函数的简要描述:

  • submitNewPost(@Query("id") id: Long, @Field("text") text: String) 提交一个新的帖子,提交新帖子时,你需要传递用户 ID 和文本。

  • getPostList() 获取所有帖子。

  • getPostById(@Query("id") id: Long) 获取一个帖子。你需要传递帖子 ID。

创建评论服务

为了处理评论 REST API,我们将创建两个 HTTP 请求。因此,我们将使用 Retrofit 注解创建两个 POSTDELETE 请求。现在创建一个名为 PostService.kt 的接口,以下是代码:

interface CommentService {
    // Post comment in a post by Profile ID and Post ID
    @POST("/comment/{user_id}/{post_id}")
    fun postCommentByPostId(@Path("post_id") postId: Long, @Path("user_id") userId: Long,
                            @Query("commentText") commentText: String): Observable<Post>

    // Delete comment in a post by Profile ID and Post ID
    @DELETE("/comment/{user_id}/{post_id}")
    fun deleteCommentByPostId(@Path("post_id") postId: Long, @Path("user_id") userId: Long,
                              @Query("commentText") commentText: String): Observable<Post>
}

postCommentByPostId(@Path("post_id") postId: Long, @Path("user_id") userId: Long,

@Query("commentText") commentText: String) 是一个 POST 请求函数,用于提交一个新的评论。你需要传递 user_idpost_id 和文本。

deleteCommentByPostId(@Path("post_id") postId: Long, @Path("user_id") userId: Long,

@Query("commentText") commentText: String) 是一个 DELETE 请求函数,用于删除评论。你需要传递 user_idpost_id

到目前为止,所有请求都已创建,现在我们需要创建一个 API 服务,该服务将击中服务器并获取 JSON。

创建 API 服务

我们在 第四章 中解释了此过程,Android 的 Spring 模块。因此,我们只需向您展示代码并解释新功能。创建一个名为 APIService.kt 的对象,并添加 gsonConverter()getOkhttpClient(username, password)

object APIService{
   fun getRetrofitBuilder(username:String, password:String): Retrofit {
       return Retrofit.Builder()
           .client(getOkhttpClient(username, password))
           .baseUrl(Constants.API_BASE_PATH)
           .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
           .addConverterFactory(gsonConverter())
           .build()
    }

    fun gsonConverter(): GsonConverterFactory {
        return GsonConverterFactory
            .create(
                GsonBuilder()
                    .setLenient()
                    .disableHtmlEscaping()
                    .create()
            )
    }

    fun getOkhttpClient(profileName: String, password: String): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(BasicAuthInterceptor(profileName, password))
            .connectTimeout(30, TimeUnit.SECONDS)
            .readTimeout(60, TimeUnit.SECONDS)
            .build()
    }
}

如果您对addInterceptor(BasicAuthInterceptor(profileName, password))感到困惑,请前往第五章使用 Spring Security 保护应用程序,并查看名为使用 OkHttp 拦截器进行身份验证的部分。

现在,我们需要初始化服务的RetrofitBuilder函数。我们有四个服务接口,现在我们将为它们创建四个RetrofitBuilder函数。在APIService.kt文件中添加以下代码:

// get profile request builder
fun profileAPICall(username:String, password:String) = getRetrofitBuilder(username, password)
    .create(ProfileService::class.java)

// get post request builder
fun postAPICall(username:String, password:String) = getRetrofitBuilder(username, password)
    .create(PostService::class.java)

// get comment request builder
fun commentAPICall(username:String, password:String) = getRetrofitBuilder(username, password)
    .create(CommentService::class.java)

现在,我们将专注于前端,这意味着活动和布局。

创建登录活动

这是应用程序的第一个活动。当用户进入应用程序时,它将是他们看到的第一件事。对于用户来说,他们需要转到注册活动来注册新配置文件。注册后,他们将能够访问应用程序。

修改布局

创建一个名为LoginActivity.kt的空活动和一个名为activity_login.xml的布局。以下是xml中的代码(您可以在 GitHub 上找到该布局的完整版本):

------
------
<android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_alignParentStart="true"
        android:layout_centerHorizontal="true"
        android:background="@color/reg_body"
        app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
        android:layout_marginBottom="64dp" app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginStart="32dp" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="32dp"
        android:id="@+id/cardView">

    <android.support.constraint.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintVertical_bias="1.0" android:layout_marginEnd="24dp"
            android:layout_marginTop="32dp"
            android:layout_marginStart="24dp" android:layout_marginBottom="32dp">
        <TextView
                android:id="@+id/LogIn"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentTop="true"
                android:layout_centerHorizontal="true"
                android:text="@string/title_login"
                android:textSize="30sp"
                android:textStyle="bold"
                android:typeface="monospace" app:layout_constraintEnd_toEndOf="parent"
                android:layout_marginEnd="8dp" app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"/>
------
------
 <Button android:layout_width="match_parent" android:layout_height="wrap_content"
                    android:text="@string/title_login"
                    android:id="@+id/reg_submit"

                    app:layout_constraintTop_toBottomOf="@+id/password_title_reg"
                    app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
                    android:layout_marginEnd="32dp" android:layout_marginStart="32dp"
                    android:layout_marginTop="64dp"/>
------
------ 

在这里,我们有用户输入的UsernamePassword。在这个布局中,我们还有一个登录按钮和一个TextView,用于转到RegistrationActivity

这里是这个布局的图片预览:

图片预览

修改活动

前往LogInActivity.kt文件,我们将在这里输入登录信息。用户需要提供一个usernamepassword。然后这些信息将在服务器数据库的Profile表中进行搜索。如果在这个Profile表中存在相同的usernamepassword,您将能够进入MainActivity,或者您将收到错误消息。如果您是新用户,您可以点击“New Member?”来注册新配置文件。

首先,我们将检查SharedPreferences以查看我们是否保存了usernamepassword。它将显示在用户名和密码字段中,或者它将保持空白,以便您可以输入值。以下是这个逻辑的功能:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_login)

   setUsernamePassword()
 }

private fun setUsernamePassword() {
 if (PrefUtils.getUsername(this) != null
 || PrefUtils.getPassword(this) != null) {
 username_input_login.setText(PrefUtils.getUsername(this))
 password_input_login.setText(PrefUtils.getPassword(this))
 }
}

现在,在名为need_regTextView上设置OnClickListener()监听器函数,它将带我们到RegistrationActivity。以下是该函数的代码:

need_reg.setOnClickListener {
    val intent = Intent(this, RegistrationActivity::class.java)
    startActivity(intent)
}

登录请求

现在,我们将创建一个名为logInUser()的函数,该函数将向服务器发送POST请求并匹配usernamepassword。如果失败,它将获取错误并显示错误消息,或者它将带您到MainActivity。以下是该函数的代码:

private fun logInUser(){

    APIClient.profileAPICall(username_input_login.text.toString(), password_input_login.text.toString())
        .loginProfile(username_input_login.text.toString(),password_input_login.text.toString() )
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe({
                newUser ->
            if(newUser.error != null){
                Toast.makeText(applicationContext,newUser.error!!, Toast.LENGTH_SHORT).show()
            }else {
                PrefUtils.storeUsernameID(this, newUser.userID!!)
                PrefUtils.storeUsername(this, newUser.username!!)
                PrefUtils.storePassword(this, newUser.password!!)
                username_input_login.setText(PrefUtils.getUsername(this))
                password_input_login.setText(PrefUtils.getPassword(this))
                val intent = Intent(this, MainActivity::class.java)
                startActivity(intent)
            }
        },{
                error ->
            Toast.makeText(applicationContext,R.string.err_login_msg, Toast.LENGTH_SHORT).show()
            Log.wtf("******", error.message.toString())
        })
}

在这里,如果我们得到正确的响应,我们将存储usernamepassworduserID

创建注册活动

创建一个名为RegistrationActivity.kt的注册活动,我们将在这里注册新账户。在修改代码之前,我们需要修改布局。

修改布局

RegistrationActivity.kt创建一个名为activity_registration.xml的布局。在这里,我添加了 UI,请查看 GitHub 上的完整版本文件。以下是该文件中的一段代码:

 <Button android:layout_width="match_parent" android:layout_height="wrap_content"
                        android:text="@string/title_reg"
                        android:id="@+id/reg_submit" android:layout_marginTop="32dp"
                        app:layout_constraintTop_toBottomOf="@+id/country_title_reg"
                        app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="32dp"
                        app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="32dp"/>

          -----
          -----

            </android.support.constraint.ConstraintLayout>
        </ScrollView>
    </android.support.v7.widget.CardView>

</android.support.constraint.ConstraintLayout>

这是此布局的图像预览:

图片

修改活动

这是RegistrationActivity的代码:

class RegistrationActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_registration)

    }
}

现在添加一些逻辑来验证用户名密码电子邮件 ID。以下是代码:


private fun validateName(): Boolean {
    if (username_input_reg.text.toString().trim().isEmpty()) {
        username_title_reg.error = getString(R.string.err_msg_name)
        requestFocus(username_input_reg)
        return false
    } else {
        username_title_reg.isErrorEnabled = false
    }

    return true
}

private fun validateEmail(): Boolean {
    if (email_input_reg.text.toString().trim().isEmpty() || !isValidEmail(email_input_reg.text.toString().trim())) {
        email_title_reg.error = getString(R.string.err_msg_email)
        requestFocus(email_input_reg)
        return false
    } else {
        email_title_reg.isErrorEnabled = false
    }

    return true
}

private fun validatePassword(): Boolean {
    if (password_input_reg.text.toString().trim().isEmpty()
    || con_password_input_reg.text.toString().trim().isEmpty()) {

        if (password_input_reg.text.toString().trim()
            == con_password_input_reg.text.toString().trim()){
            password_title_reg.error = getString(R.string.err_match_password)
            requestFocus(password_title_reg)
        }

        password_title_reg.error = getString(R.string.err_msg_password)
        requestFocus(password_title_reg)
        return false
    } else {
        password_title_reg.isErrorEnabled = false
    }

    return true
}

添加一个TextWatcher内部类,如果输入有任何无效内容,它将发送一个警报:

private inner class MyTextWatcher (private val view: View) : TextWatcher {

    override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}

    override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}

    override fun afterTextChanged(editable: Editable) {
        when (view.id) {
            R.id.username_input_reg -> validateName()
            R.id.email_input_reg -> validateEmail()
            R.id.input_password -> validatePassword()
        }
    }
}

用户名密码电子邮件 ID无效时,它会显示一个警报。

注册新资料

现在我们将创建一个名为registerUser()的函数,该函数将帮助你向服务器发送请求并从服务器获取输出。我们将在第八章反应式编程和第四章 Android 的 Spring 模块中展示如何使用 RxJava。以下是registerUser()的代码:

private fun registerUser(){
    val newProfile = Profile(null,
        username_input_reg.text.toString(),
        password_input_reg.text.toString(),
        email_input_reg.text.toString(),
        null,
        first_name_input_reg.text.toString(),
        last_name_input_reg.text.toString(),
        contact_input_reg.text.toString(),
        country_input_reg.text.toString())

        APIClient.profileAPICall("","")
        .registerProfile(newProfile)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe({
                newUser ->
           if(newUser.duplicate != null){
                    Toast.makeText(applicationContext,newUser.duplicate!!, Toast.LENGTH_SHORT).show()
                }else {
                    PrefUtils.storeUsernameID(this, 1)
                    PrefUtils.storeUsername(this, username)
                    PrefUtils.storePassword(this, password)
                    val intent = Intent(this, LoginActivity::class.java)
                    startActivity(intent)
                }

        },{
                error ->
                            Toast.makeText(applicationContext,error.message.toString(), Toast.LENGTH_SHORT).show()

        })
}

在这里,我们将从EditText获取内容并创建一个Profile对象。然后我们获取一个观察者,它将作为 JSON 类型获取配置文件列表,并在subscribe()函数中处理更新的列表。如果结果是完整的,它将在第一个参数中返回,然后我们将使用SharedPreferences本地保存用户名密码userID并返回到LoginActivity。如果抛出错误,它将进入第二个参数。

修改主活动

这是我们的主页。在这里,你可以看到所有帖子。我们需要修改我们的布局和活动类。

修改布局

MainActivity的布局在activity_main.xml文件中。在这里,我们添加了RecyclerView以显示列表,一个FabButton用于提交帖子,以及一个TextView以显示如果没有帖子可用。以下是代码片段:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.MainActivity">
-----
-----

<android.support.v7.widget.RecyclerView
 android:id="@+id/displayList"
 android:layout_width="0dp"
 android:layout_height="0dp"
 app:layout_constraintEnd_toEndOf="parent"
 app:layout_constraintStart_toStartOf="parent"
 tools:listitem="@layout/post_item"
 app:layout_constraintBottom_toBottomOf="parent"
 app:layout_constraintHorizontal_bias="0.0" android:layout_marginTop="8dp"
 app:layout_constraintTop_toBottomOf="@+id/appBarLayout"/>
 </android.support.constraint.ConstraintLayout>
     <android.support.design.widget.FloatingActionButton
     android:id="@+id/fabMain"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_gravity="bottom|end"
     android:layout_margin="@dimen/fab_margin"
     app:srcCompat="@android:drawable/ic_dialog_email"/>
</android.support.design.widget.CoordinatorLayout>

修改活动

前往MainAcitivty.kt。在这里,我们有RecycleView和帖子适配器。我们将在onCreate()函数中添加一个全局的List<Post>并将recycleView设置如下:

private var postList: List<Post> = listOf()

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
        displayList.layoutManager = LinearLayoutManager(this)
        displayList.setHasFixedSize(true)
        postRecycleViewAdapter = PostRecycleViewAdapter(this, postList)
        displayList.adapter = postRecycleViewAdapter
}

在这里,我们已初始化名为postRecycleViewAdapterPostRecycleViewAdapter,并将适配器设置到名为displayList的列表中。

获取帖子

我们将使用getAllPosts()函数获取所有帖子。此函数将向服务器发送请求以获取所有帖子列表。作为回报,我们将得到名为newPostList的更新列表,并使用setItems(newPostList)传递给PostRecycleViewAdapter,并使用notifyDataSetChanged()通知。对于错误处理,我们使用了 toast。以下是getAllPosts()函数的代码:

private fun getAllPosts() {
        APIClient.postAPICall(PrefUtils.getUsername(this)!!, PrefUtils.getPassword(this)!!)
        .getPostList()
            .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe({
            newPostList ->
            postRecycleViewAdapter.setItems(newPostList)
 postRecycleViewAdapter.notifyDataSetChanged()
        },{
                error ->
       Toast.makeText(applicationContext, error.message.toString(), Toast.LENGTH_SHORT).show()
        })
}

提交帖子

当你按下 fab 按钮时,我们会看到一个输入状态的警报框,你可以使用submitPost()输入你的状态。作为回报,我们得到名为newPostList的帖子列表,并将其传递给PostRecycleViewAdaptersetItems(newPostList)以替换旧的帖子列表。最后,使用notifyDataSetChanged()通知,RecycleView列表将被更新。

这是submitPost()函数的代码:

private fun submitPost(id: Long, text: String){
    APIClient.postAPICall(PrefUtils.getUsername(this)!!, PrefUtils.getPassword(this)!!)
        .submitNewPost(id, text)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe({
                newPostList ->
            postRecycleViewAdapter.setItems(newPostList)
            postRecycleViewAdapter.notifyDataSetChanged()
        },{
                error ->
      Toast.makeText(applicationContext, error.message.toString(), Toast.LENGTH_SHORT).show()
        })
}

实现菜单

要显示配置文件详情和更新帖子,我们将在 Toolbar 上添加两个图标。为此,我们需要创建一个工具栏文件。在 res > menu 中创建一个名为 menu_main.xml 的菜单文件。在那里我们将添加两个项目,一个用于配置文件,一个用于更新帖子。

下面是 menu_main.xml 的代码:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
            android:id="@+id/profileMenu"
            android:icon="@drawable/ic_face_white_24dp"
 app:showAsAction="always"            android:title="@string/title_profile">
    </item>
    <item
            android:id="@+id/postUpdate"
            android:icon="@drawable/ic_autorenew_white_24dp"
 app:showAsAction="always"            android:title="@string/title_update">
    </item>
</menu>

我们使用了 app:showAsAction="always",这意味着项目将始终显示在工具栏上。

现在在 MainAcitivy.kt 中实现它。为此,我们需要重写两个函数,这两个函数是 onCreateOptionsMenu()onOptionsItemSelected()

我们将在 onCreateOptionsMenu() 中使用 menuInflater.inflate() 绑定 menu_main 菜单 XML 文件,并在 onOptionsItemSelected() 中写下每个菜单项的逻辑:

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        when (item!!.itemId) {
            R.id.profileMenu -> {
                val intent = Intent(this, ProfileActivity::class.java)
                startActivity(intent)
            }
            R.id.postUpdate -> {
                getAllPosts()
            }
        }
        return true
    }

R.id.profileMenu 将带您进入 ProfileActivity 类。

R.id.postUpdate 将使用 getAllPosts() 更新帖子。

修改帖子适配器

现在我们需要修改我们的帖子适配器类。它将帮助我们以良好的结构显示帖子。我们的帖子适配器名称是 PostRecycleViewAdapter,布局名称是 post_item

修改帖子适配器布局

要利用帖子适配器,我们需要创建一个名为 post_item.xmlxml 文件,在这里我们将实现 UI。下面是一段代码(完整代码可以在 GitHub 上找到):

----
----
        <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
                  tools:text="@tools:sample/date/ddmmyy"
                  android:id="@+id/postedDate"
                  android:textAppearance="?android:textAppearanceSmall"
                  app:layout_constraintTop_toBottomOf="@+id/profileName"
                  app:layout_constraintStart_toStartOf="@+id/profileName"
                  android:layout_marginTop="4dp"
                  app:layout_constraintBottom_toBottomOf="parent" android:layout_marginBottom="4dp"/>
    </android.support.constraint.ConstraintLayout>

    <TextView android:layout_width="0dp" android:layout_height="wrap_content"
              tools:text="@tools:sample/lorem"
              android:id="@+id/postText"
              android:padding="4dp"
              android:textAppearance="?android:textAppearanceSmall"
              app:layout_constraintStart_toStartOf="parent"
              app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="4dp"
              app:layout_constraintTop_toBottomOf="@+id/constraintLayout"/>
----
----

我们有四个 TextView 用于用户全名、用户名、发布时间和帖子文本。

下面是布局预览选项的一个示例图像:

创建帖子适配器

让我们创建一个名为 PostRecycleViewAdapter.kt 的自定义 RecycleView 适配器来显示帖子列表。我们已经展示了如何在 第四章,Spring Modules for Android 中创建自定义适配器,所以我们将不再重复。以下是 PostRecycleViewAdapter 类:

class PostRecycleViewAdapter(private var context: Context,
                       private val postList: List<Post>):
RecyclerView.Adapter<PostRecycleViewAdapter.ViewHolder>() {
-----
-----
}

现在在 PostRecycleViewAdapter.kt 中创建 ViewHolder 类,并初始化 post_item 布局的所有内容,如下面的代码所示:

class ViewHolder(view: View): RecyclerView.ViewHolder(view){
    val postRoot = view.findViewById(R.id.postRoot) as ConstraintLayout

    val profileFullName = view.findViewById(R.id.profileFullNamePost) as TextView
    val username = view.findViewById(R.id.usernamePost) as TextView
    val postedDate = view.findViewById(R.id.postedDate) as TextView
    val postText = view.findViewById(R.id.postText) as TextView
}

现在重写 onCreateViewHolder() 并返回 ViewHolder 类:

override fun onCreateViewHolder(viewGroup: ViewGroup, p1: Int): ViewHolder {
    val layoutInflater = LayoutInflater.from(context).inflate(R.layout.post_item, viewGroup, false)
    return ViewHolder(layoutInflater)
}

现在,我们需要根据其位置设置列表中每一行的值。为此,重写 onBindViewHolder() 函数并添加以下代码:

override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {

    val userDetails = postList[position]

    viewHolder.profileFullName.text = "${userDetails.profile!!.firstName} ${userDetails.profile!!.lastName} "
    viewHolder.username.text = userDetails.profile!!.username
    viewHolder.postedDate.text = userDetails.postCreatedTime
    viewHolder.postText.text = userDetails.text

}

修改配置文件布局

这个布局将帮助从用户那里获取配置文件详情。打开 activity_profile.xml 并按以下方式修改(请检查 GitHub 上的完整布局代码):

<?xml version="1.0" encoding="utf-8"?>

    <!--full name-->

    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
              android:id="@+id/profileFullNameTitlePro"
              android:textStyle="bold"
              android:text="@string/title_full_names"
              android:textAppearance="?android:textAppearanceSmall"
              android:layout_marginStart="8dp"
              app:layout_constraintStart_toStartOf="parent" android:layout_marginTop="32dp"
              app:layout_constraintTop_toBottomOf="@+id/usernamePro" android:layout_marginEnd="8dp"
              app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0"/>

    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
              tools:text="@tools:sample/full_names"
              android:id="@+id/profileFullNamePro"
              android:textAppearance="?android:textAppearanceSmall"
              app:layout_constraintTop_toTopOf="@+id/profileFullNameTitlePro"
              app:layout_constraintBottom_toBottomOf="@+id/profileFullNameTitlePro"
              app:layout_constraintEnd_toEndOf="parent"
              android:layout_marginEnd="160dp"
              app:layout_constraintVertical_bias="1.0"/>

------
------
</android.support.constraint.ConstraintLayout>

这里有一个用于用户名的 TextView,每个配置文件项标签名称的 TextView,以及用于 Full NameEmailContact NumberCountry 的配置文件内容的四个 TextView

下面是 配置文件 详情的预览:

修改配置文件活动

创建一个新的活动名为 ProfileActivity.kt,以下是代码:

class ProfileActivity : AppCompatActivity() {

        private var username: String = ""
    private var password: String = ""

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_profile)
        setTitleName() 

        username = PrefUtils.getUsername(this)!!
        password = PrefUtils.getPassword(this)!!
    }
}

获取配置文件详情

要获取个人资料详情,我们需要创建一个名为 getUser() 的函数,在其中我们将从 Profile 服务中调用 getUserById()。作为回报,它将提供用户详情,或者如果有错误,它将显示错误消息。以下是 getUserById() 函数的代码:

private fun getUser(){
        APIClient.profileAPICall(username,password)
            .getUserById(PrefUtils.getUsernameID(this)!!)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({
                    myUser ->

                usernamePro.text = myUser.username
                profileFullNamePro.text = "${myUser.firstName} ${myUser.lastName}"
                emailPro.text = myUser.email
                contactNumberPro.text = myUser.contactNumber
                countryPro.text = myUser.country
            },{
                    error ->
                UtilMethods.hideLoading()
                Log.wtf("******", error.message.toString())
            })
    }

帖子详情活动

现在我们需要我们的最后一个活动,PostDetailsActivity.kt,布局在 activity_post_details.xml 中。在这个活动中,你将看到一个特定的帖子及其评论。你也可以发表评论。

修改帖子详情布局

这个视图将显示具体的帖子详情。以下是 activity_post_details.xml 中的一个代码片段:

-----
----
<android.support.v7.widget.RecyclerView
        android:id="@+id/displayList_com"
        android:layout_width="0dp"
        android:layout_height="0dp"
        tools:listitem="@layout/post_item"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/postRoot_pd"
        android:layout_marginBottom="8dp" app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginStart="16dp" android:layout_marginEnd="16dp" android:layout_marginTop="8dp"/>

<android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:id="@+id/postRoot_pd"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        app:layout_constraintTop_toBottomOf="@+id/appBarLayout_pd" app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp">
-----
----

这里有一个帖子的详情及其评论列表。

这个布局的预览如下:

修改帖子详情活动

这是一个处理特定帖子的活动。这个帖子将通过 postId 获取,我们将通过从 PostRecycleViewAdapter 发送的 intent 获取它。要获取 intent 视图,我们需要使用 intent.extras。我们使用 Long"postId"键名,如下面的代码所示:

private var postId:Long = -1

if(intent.extras!=null){
    postId = intent.extras.getLong("postId")
}

获取帖子详情

现在创建一个名为 getPostById(id: Long) 的函数,我们将从 MainActivity 传递给定的 postId。我们将处理特定的 TextView 中的所有值,例如 MainActivity

@SuppressLint("CheckResult")
private fun getPostById(id: Long){
    UtilMethods.showLoading(this)
    APIClient.postAPICall(PrefUtils.getUsername(this)!!, PrefUtils.getPassword(this)!!)
        .getPostById(id)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe({
                post ->
            postText_pd.text = post.text
            profileFullNamePost_pd.text = "${post.profile!!.firstName} ${post.profile!!.lastName}"
            usernamePost_pd.text = post.profile!!.username
            postedDate_pd.text = SimpleDateFormat(Constants.TIME_FORMAT).format(post.postCreatedTime!!)

            commentList = post.comment!!

            Log.wtf("******", commentList.toString())
            commentRecycleViewAdapter.setItems(commentList)
            commentRecycleViewAdapter.notifyDataSetChanged()

            UtilMethods.hideLoading()
        },{
                error ->
            UtilMethods.hideLoading()
            Log.wtf("******", error.message.toString())
            Toast.makeText(applicationContext, error.message.toString(), Toast.LENGTH_SHORT).show()
        })
}

提交评论

要提交评论,点击 fabButton 并输入评论。评论提交系统与帖子提交系统类似。我们创建一个名为 submitComment(id: Long, text: String) 的函数,并使用它来提交评论。以下是 submitComment() 函数:

@SuppressLint("CheckResult")
private fun submitComment(id: Long, text: String){
    UtilMethods.showLoading(this)
    APIClient.commentAPICall(PrefUtils.getUsername(this)!!, PrefUtils.getPassword(this)!!)
        .postCommentByPostId(id, PrefUtils.getUsernameID(this)!!,text)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe({
                newPostList ->
            commentList = newPostList.comment!!

            Log.wtf("******", commentList.toString())
            commentRecycleViewAdapter.setItems(commentList)
            commentRecycleViewAdapter.notifyDataSetChanged()
            UtilMethods.hideLoading()
        },{
                error ->
            UtilMethods.hideLoading()
            Log.wtf("******", error.message.toString())
            Toast.makeText(applicationContext, error.message.toString(), Toast.LENGTH_SHORT).show()
        })
}

修改评论适配器

这个适配器与帖子适配器相同。检查 修改帖子适配器, 修改帖子适配器布局 以修改这个评论适配器。这个适配器的名称是 CommentRecycleViewAdapter.kt,布局是 comment_item.xml

我们的项目完成了!现在,是时候检查服务器和客户端的输出了。

检查输出

要检查我们的输出,首先,从 Social_Network Spring 项目运行服务器。然后你可以运行两个不同的模拟器或 Android 设备作为客户端用户。

现在打开 Android 应用。点击“新成员?”按钮创建新账户。填写所有必要的详细信息并点击“注册”按钮:

如果用户名已被占用,系统将像这样提醒你:

现在如果你按下工具栏上的第二个左边的“Profile”按钮,你将看到个人资料详情。如果你按下顶部的“Update”按钮,即工具栏左上角的图标,你的帖子将更新,如下面的截图所示:

点击任何帖子,你将看到具体的帖子,并且你可以使用 Fab 按钮添加评论:

我们已经到达了这段漫长旅程的终点。现在,您的客户端应用程序已经准备好使用。在这里,您可以发布状态,查看发布的状态,检查帖子的详细信息,并在该帖子上发表评论。我们已经向您展示了如何在 Android 应用程序中使用服务器和处理来自服务器的资源。您可以在 GitHub 上找到一些可能有助于此应用程序的次要功能和布局。我们建议您发挥自己的想象力创建另一个应用程序并部署它。这将更加有效,您还可以学习更多您想学的内容。处理 HTTP 请求有众多替代方法,因此您可以学习更多。如果您想了解更多,请查看进一步阅读部分。

摘要

经过漫长的旅程,我们已经完成了这一章。在这里,我们看到了如何使用前几章的所有模块(如 Spring Security 和数据库)创建一个服务器端和客户端的完整应用程序。您可以根据自己的风格修改这个项目。您可以实现新的架构和实现新的框架。在本章中,首先,我们学习了项目的架构设计。然后,我们为我们的项目创建了数据库模型。在创建数据库模型之后,我们创建了我们的服务器端项目并实现了依赖关系。然后,我们根据数据库模型创建了模型。然后,我们创建了存储库和控制器。在控制器中,我们创建了 HTTP 请求函数。然后,我们实现了安全性。我们使用了 Spring Security 中的基本身份验证。然后,我们修改了 application.properties 以连接到 MySQL 数据库。在完成服务器端之后,我们开始创建 Android 应用程序。我们创建了应用程序的工作流程。然后,我们创建了用户、帖子评论的模型。然后,我们创建了 API 服务和请求。在后台开发之后,我们开发了布局和活动。在活动中,我们使用 Retrofit 和 RxJava 调用了 HTTP 请求。然后,我们修改了应用程序的 UI 类和布局。最后,我们测试了项目的输出。

在第十章“测试应用程序”中,您将学习如何使用 JUnit 框架对 Spring 项目进行单元测试,以及在 Android 应用程序中使用 Espresso 进行 UI 测试。

问题

  1. 什么是 EER 图?

  2. CRUD 操作是什么?

  3. 哪种类型的工具可以用作 HTTP 客户端?

  4. 目前,Android 的最小、最大和目标 API 版本是什么?

  5. Android 架构的常见名称是什么?

  6. 开发 Android 应用程序时,有哪些模拟器的名称?

进一步阅读

第十章:测试应用程序

要使应用程序更易于使用和吸引人,我们总是专注于标志、内容、UI、体验等方面,此外,我们还注意代码风格。我们使用最新的架构和框架来减少代码行数和样板代码,以创建一个健壮、简单和快速的应用程序。然而,许多开发者忘记了测试阶段。有些人可能直到应用程序使用过程中生成崩溃报告时才意识到有问题,因为他们没有在项目中进行充分的测试。通常,一些开发者会跳过测试,因为他们不想花费额外的时间编写在项目中没有直接使用的测试用例。这是一个常见的错误,会导致质量下降。

随机崩溃的应用程序总是不受用户欢迎,这就是为什么最成功的 Android 应用程序总是经过彻底的测试。深入测试可以消除应用程序的缺陷,优化内存使用,同时还能让你改善应用程序的功能行为、可用性和正确性。

在本章中,我们将介绍测试及其在 Spring 和 Android 中的应用。本章涵盖了以下主题:

    • 软件测试

    • 测试基础

    • Spring Boot 的单元测试

    • 创建项目

    • JUnit

    • Android 的 UI 测试

    • Espresso

技术要求

你将需要导入 Spring 和 Android 的依赖项。以下是依赖项。

  • Spring

要实现测试的依赖,你需要在 pom.xml 文件中添加测试依赖:

<!-- This is to implement the testing functions for the spring project -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
</dependency>
  • Android

为了测试 Android 项目,我们需要在 gradle 文件中实现测试依赖。要添加依赖,我们需要在 build.gradle (app 模块) 文件的 dependencies {...} 中实现。以下是这个 build.gradle 文件的代码片段:

// Dependencies for local unit tests
dependencies{
testImplementation "junit:junit:$rootProject.ext.junitVersion"

// Espresso UI Testing dependencies.
androidTestImplementation "com.android.support.test.espresso:espresso-core:$rootProject.ext.espressoVersion"
androidTestImplementation "com.android.support.test.espresso:espresso-contrib:$rootProject.ext.espressoVersion"
androidTestImplementation "com.android.support.test.espresso:espresso-intents:$rootProject.ext.espressoVersion"
}

本章的示例源代码可在 GitHub 上找到,链接如下:github.com/PacktPublishing/Learn-Spring-for-Android-Application-Development/tree/master/Chapter10

软件测试

软件测试是任何项目中最基本的部分之一。测试评估稳定性、可用性、质量保证、组件的功能,并确保软件可以发布到市场。它还有助于找出项目的错误、缺失的需求等。测试使用技术来执行应用程序或程序中的某些过程,目的是找出错误。

在第六章“访问数据库”和第九章“创建应用”中,我们创建了 Spring 应用并实现了 REST API。然后我们使用一个名为 Insomnia 的第三方工具进行测试。之后,我们提到了使用 HTTP CRUD 请求函数的 URL 路径并检查了输出。这个系统运行良好,我们可以直接看到输出。然而,通常很难找到错误和 bug,因为它不能显示错误或任何异常行为。尽管这个项目正在运行,但并不一定安全地发布到市场上。因此,我们需要进一步测试其稳定性。让我们运行两个流行的测试工具和框架,JUnit 和 Espresso。

JUnit

JUnit 是最受欢迎的 Java 测试框架,它是开源的,几乎拥有测试基于 Java 的应用程序所需的全部功能和模块,适用于测试驱动开发环境。JUnit 主要关注编写针对特定类或函数的自动化测试。它有助于调用函数并检查预期的输出。在看到 JUnit 的一些使用示例之前,让我们了解一下它的优势。

JUnit 的优势

JUnit 因其用户友好的功能而广泛用于测试 Java 应用。它具有一些强大的优势,如下所示:

  • JUnit 框架是开源的

  • 它提供基于文本的命令行以及基于 AWT 和 Swing 的图形测试机制

  • 它有一些注解来利用测试函数

  • 它有一个测试运行器来测试运行中的应用

  • 它允许你编写代码

  • 它可以自动测试并提供反馈

JUnit 的基本注解

JUnit 有一些基本且重要的注解,如下所示:

  • @BeforeClass: 这个方法在类中的任何测试函数之前运行一次。在这个函数中,你可以连接数据库或连接池。这个方法必须是静态方法。

  • @AfterClass: 这个方法在类中的任何测试函数之后运行一次。在这个函数中,你可以关闭数据库连接并进行清理。

  • @Before: 这个方法可以在@Test注解的函数之前运行。在这里,你可以创建一些对象,并将它们共享给所有@Test注解的测试函数。

  • @After: 这个方法可以在@Test注解的函数之后运行。在这里,你可以修改或清理对象,并将它们共享给所有@Test注解的测试函数。

  • @Test: 这个被注解的函数是测试函数。

现在我们将看看如何使用 JUnit 测试一个项目的示例。在这里,你可以了解测试注解的生命周期以及这些注解的使用。

创建一个项目

让我们创建一个项目,在这个项目中,我们将使用数据库创建 REST API,并展示用户详情列表。在这个项目中,我们将使用 JDBC、MySQL 和 Spring Boot。

要创建项目,请访问 start.spring.io 并创建一个基于 Kotlin 的项目,包含以下给定的依赖项:

  • Web

  • JDBC

  • MySQL

  • DevTools

现在我们将创建一些示例代码,并可以测试它们。

使用 JUnit 测试项目

打开我们之前生成的项目,按照以下步骤操作:

  1. 前往测试 | kotlin | com.packtpub.sunnat629.testing_application,如下截图所示:

图片

  1. 现在创建一个名为 JUnitTestClass.kt 的类,我们将使用注解创建一些测试用例。以下是一个示例代码:
class JUnitTestClass {

    companion object {
        @BeforeClass
        @JvmStatic
        fun runBeforeClass(){
            println("============ @BeforeClass ============\n")
        }

        @AfterClass
        @JvmStatic
        fun runAfterClass(){
            println("============ @AfterClass ============")
        }
    }

    @Before
    fun runBefore(){
        println("============ @Before ============")
    }

    @After
    fun runAfter(){
        println("============ @After ============\n")
    }

    @Test
    fun runTest1(){
        println("============ @TEST One ============")
    }

    @Test
    fun runTest2(){
        println("============ @TEST Two ============")
    }
}

你可以看到我们在 companion object {} 中编写了 @BeforeClass@AfterClass 注解的函数,这意味着这些函数是静态的。在 Kotlin 中,你必须将静态变量和函数写入 companion object {}

我们使用了 @JvmStatic 注解。这在 Kotlin 中特别用于指定这个函数是静态的,并且需要在这个函数的元素中生成。

  1. 现在通过点击函数名旁边的运行测试图标来运行这个测试,如下截图所示:

图片

在运行所有测试用例后,它将显示结果;即,通过或失败。以下是输出:

图片

这里你可以看到我们有两个测试用例名为 runTest1runTest2 已经通过了测试。

  1. 现在修改我们的 runTest1 函数并编写逻辑:
@Test
fun runTest1(){
    println("============ @TEST One Start ============")
    assertEquals(6, doSum(3,2))
    println("============ @TEST One End ============")
}

private fun doSum(num1: Int, num2: Int): Int{
        return num1 + num2
    }

在这里,我们做了一个非常简单的方程来检查测试函数。我们使用了 Assert 类的方法。assertEquals() 是断言的一个方法,主要检查两个输入的相等性。在这里,例如,我们提供了 6 和 (2+3),这不是真的,它将显示一个错误。

如果方程式正确,那么你将看到测试通过,或者它将显示一个带有预期结果的错误。以下是结果的样子:

图片

有很多断言方法。以下是一些例子:

  • assertArrayEquals:这将返回两个数组类型输入的相等性

  • assertEquals:这将返回两个相同类型输入(如 intlongdoubleString 等)的相等性

  • assertTrue:这将断言给定的条件为 true

  • assertFalse:这将断言给定的条件为 false

  • assertNotNull:这将断言给定的对象不为空

  • assertNull:这将断言给定的对象为空

为 Rest API 创建测试用例

现在我们将看到如何使用 Spring 项目的 JPA 和 Hibernate 测试数据库。以下是使用 JPA 测试数据库的步骤:

  1. 打开social_network项目。链接在这里:github.com/PacktPublishing/Learn-Spring-for-Android-Application-Development/tree/master/Chapter09/social_network

  2. 现在转到test | kotlin | com.packtpub.sunnat629.social_network包,并创建一个名为ProfileRepositoryTest.kt的文件,带有名为@RunWith(SpringRunner::class)@DataJpaTest的两个注解。

这里是ProfileRepositoryTest.kt的代码:

@RunWith(SpringRunner::class)
@DataJpaTest
class ProfileRepositoryTest {

    @Autowired
    private lateinit var entityManager: TestEntityManager

    @Autowired
    private lateinit var profileRepository: ProfileRepository

    @Test
    fun getUserTesting(){
        val newProfile = getNewProfile()
        val saveProfile = entityManager.merge(newProfile)

        val foundProfile = profileRepository.getOne(saveProfile.id!!)

        assertThat(foundProfile.username)
                .isEqualTo(saveProfile.username)
    }

    private fun getNewProfile(): Profile {
        return Profile( "naruto",
                "12345",
                "naruto123@gmail.com",
                "Naruto",
                "Uzumak")
    }
  }

以下是对前面代码的解释:

  • @RunWith(SpringRunner::class)是 Spring 和 JUnit 之间的连接器中的注解。它使用 Spring 的测试支持来运行 JUnit。

  • @DataJpaTest启用了 JPA 测试功能。

  • 我们自动装配了TestEntityManager,它主要是为 JPA 测试和 JPA EntityManager 的替代品设计的。

  • getUserTesting(),它带有@Test注解,是主要的测试函数。

现在,我们将插入一个演示Profile对象并检查插入是否正常工作。首先,我们必须使用getNewProfile()函数创建一个配置文件对象。

在此之后,我们将此配置文件保存为一个新的变量,例如这样:

val saveProfile = entityManager.merge(newProfile)

这里,我们使用了entityManager.merge(),这将把配置文件插入到数据库中。

我们还自动装配了profileRepository,现在使用此行通过 ID 获取插入的配置文件:

val foundProfile = profileRepository.getOne(saveProfile.id!!)

现在我们使用了assertThat()来检查给定的逻辑是否正确。在这个函数中,我们检查了创建的配置文件和获取的配置文件:

 assertThat(foundProfile.username).isEqualTo(saveProfile.username)

现在,如果有关插入或与数据库通信的错误,它将返回一个错误。

这里是我们测试的输出:

如果你提供一个错误的值,或者测试遇到错误,它将输出以下内容:

我们输入了一个配置文件名称为naruto,但我们测试了名称Uzumak,这就是为什么它不匹配。结果随后失败。

Android 上的 UI 测试

现在,人们比桌面更依赖手机。如果我们考虑 Android,数百万个应用程序都在 Play Store 和其他应用商店中。因此,测试 UI 以在应用商店中制作无 UI 错误且稳定的产品的非常重要。在测试过程中需要非常小心,因为存在各种显示尺寸的无数设备。对于后端,你可以使用 JUnit 进行测试,系统相同。但现在我们的测试将是基于 UI 的,因此我们将使用 Espresso。这是最流行的 UI 测试框架。

Espresso

Espresso 是一个开源框架,也是一个基于工具的 API,由 Google 设计。创建项目各种场景的测试用例是一种良好的实践。它有助于发现 UI 的意外结果或错误,以及用例。它自动同步测试动作与应用程序的 UI。它允许你在真实设备和模拟器上测试。但由于测试各种尺寸的显示和制造商的成本较高,因此在真实设备上的使用存在缺点。因此,模拟器是降低测试成本和时间的最佳解决方案。根据 Espresso 测试人员,几乎 99%的 Android 应用程序错误都可以通过这个框架检测到。Espresso 的 API 非常小,可预测,易于学习。如果你愿意,也可以自定义这些 API。

让我们创建一个项目并使用 Espresso 进行测试。

创建应用程序

让我们创建一个简单的 Android 应用程序作为客户端,该客户端将使用 GitHub API 检索 REST API:

  1. 首先,我们需要从 Android Studio 创建一个应用程序,并写下你的项目和公司域名。别忘了勾选“包含 Kotlin 支持”。以下截图显示了“创建 Android 项目”窗口:

图片

  1. 然后从“手机和平板”选项中选择最小 API 版本。对于这个项目,不需要添加其他选项。点击“下一步”后,在“添加活动到移动”字段中,你可以选择“空活动”,然后重命名活动名称和布局,点击“完成”。构建完成后,你就可以开始创建 Android 应用程序了。

创建项目后,我们需要实现测试的依赖项。

注入依赖项

此项目主要是为了测试 UI 应用程序,因此我们需要实现 Espresso。在build.gradle(模块—app)的dependencies{}块中写下以下行以实现 Espresso:

testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'

// Espresso UI Testing dependencies.
androidTestImplementation "com.android.support.test.espresso:espresso-core:3.0.2"
androidTestImplementation "com.android.support.test.espresso:espresso-contrib:3.0.2"
androidTestImplementation "com.android.support.test.espresso:espresso-intents:3.0.2"

然后在同一文件中,在android{}块中添加代码以实现针对 Android 包的 JUnit3 和 JUnit4 测试:

testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

AndroidJUnitRunner是工具运行器。它主要控制测试 APK、环境和所有测试启动。

现在同步项目以下载并将依赖项添加到项目中。

修改应用程序

我们已经在第九章“创建应用程序”中学习了如何基于RecyclerView创建应用程序,因此我们可以简单地回顾一下这个应用程序的概念。我们有一个包含 ID 和用户名的用户数据类。我们将向数据库中插入 100 个用户并在自定义的RecyclerView中显示。我们还使用UserItemAdapter来自定义RecyclerView

如果你想克隆此项目,请访问:github.com/PacktPublishing/Learn-Spring-for-Android-Application-Development/tree/master/Chapter10/TestingWithEspresso

在这个项目中,你可以找到MainActivity.kt,在那里你可以找到一个列表视图。这里是这个类中的一段代码:

----
----
 userLists.adapter = UserItemAdapter(this, userList)
----
----

这里,userLists是 RecyclerView,我们有一个UserItemAdapter自定义适配器,它是UserModel的适配器。在这里,UserModel代码是我们获取用户 ID 和名称的地方:

data class User(var userID: Int, var username: String)

现在,我们将使用 Espresso 测试这个列表视图,并使用我们在项目中经常使用的几个主要功能。

创建测试文件

让我们编写一些测试用例。要编写此代码,我们需要在 androidTest 包中创建新文件。为此,请按照以下步骤操作:

  1. 现在转到 src | androidTest | java | 项目的module_name。这是这个目录的截图:

  1. 创建一个名为MainActivityTest.kt的类,并带有@RunWith(AndroidJUnit4::class)注解。这个注解将测试与应用程序功能链接。

让我们创建我们的第一个 Espresso 测试:

首先,我们需要连接我们的MainActivity类。为此,我们将初始化一个ActivityTestRule<MainActivity>变量的实例,它将为MainActivity提供所有功能。它有一个@Rule注解,这意味着对单个活动的测试,这里是指MainActivity

这个getCountUser()函数是用来检查你的列表中用户数量的:

// User count Matching
@Test
fun getCountUser(){
    onView(withId(R.id.userLists))
        .check(matches(itemCount(20)))
}

在前面的代码中,我们做了以下操作:

  • ViewMatchers.onView()意味着它将采用匹配器逻辑。

  • ViewMatchers.withId()用于连接你的活动布局的组件。在我们的main_activity.xml中,RecyclerView的 ID 名称是userLists,所以我们在这里连接它。

  • check(..)将返回一个布尔值。

  • matches(itemCount(20)意味着它将匹配给定的数字与你的用户列表数量。

我们需要手动创建itemCount()。为此,创建一个名为CustomUserMatchers.kt的类。在这里,是这个类的代码:

class CustomUserMatchers {
    companion object {
        fun itemCount(count: Int): Matcher<View>{
            return object : BoundedMatcher<View, RecyclerView>(RecyclerView::class.java){
                override fun describeTo(description: Description?) {
                    description!!.appendText("Total User = $count")
                }

                override fun matchesSafely(item: RecyclerView?): Boolean {
                    return item?.adapter?.itemCount == count
                }
            }
        }
    }
}

这里,我们创建了一个CustomUserMatchers.kt类,在其中创建了一个静态函数并返回一个Matcher<View>

BoundedMatcher<View, RecyclerView>(RecyclerView::class.java)有两个函数名为describeTo(description: Description?)matchesSafely(item: RecyclerView?),并且我们已经重写了这些类。

matchesSafely中,我们将检查列表数量与给定数量的相等性。

在我们的输出列表中,我们有100个用户,但这里给出的数字是20。所以当你运行测试时,它将失败,就像这个截图所示:

如果你提供100并运行,你可以看到测试通过了,就像这个截图所示:

![](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/34ea9ddd-d353-4a8c-8d6c-67af3492cc9a.png)

现在创建一个名为getUserPosition()的测试用例来获取特定位置并点击它:

// User Click with a position number
@Test
fun getUserPosition(){
    onView(withId(R.id.userLists))
        .perform(actionOnItemAtPosition
        <RecyclerView.ViewHolder>(34, click()))
}

actionOnItemAtPosition<RecyclerView.ViewHolder> 的作用是使用 RecyclerViewViewHolder 选择 RecyclerView 列表中的某个位置,然后我们使用列表的第 34 行的 click()。这意味着这个测试将会跳转到指定的位置并点击那个项目。你可以在下面的屏幕截图看到它已经点击并显示了一个 Toast,表明测试用例已经点击了列表的第 34 行:

图片

如果你查看日志输出,你也会注意到测试已经通过。以下是 Android Studio 的日志输出:

图片

  • 创建一个 getIsDisplayed() 函数来测试给定的列表是否正在显示。

  • withId(R.id.userLists) 将获取 MainActivity 的列表视图。

  • check(matches(isDisplayed())) 检查列表是否在设备上显示:

// User list display test
@Test
fun getIsDisplayed(){
    onView(withId(R.id.userLists))
        .check(matches(isDisplayed()))
}

创建一个 getIsClickable() 函数来测试给定的列表是否正在显示。withId(R.id.userRoot) 将获取 ConstraintLayout,而 check(matches(isClickable())) 将匹配列表的点击状态:

// User list display test
@Test
fun getIsClickable(){
    onView(withId(R.id.userRoot))
        .check(matches(isClickable()))
}

创建一个 getScrollToBottom() 函数来检查如何滚动到特定位置。withId(R.id.userLists) 将获取列表视图,perform(scrollToPosition<RecyclerView.ViewHolder>(activityTestRule.activity.userLists.adapter!!.itemCount - 1)) 将滚动到列表底部。使用这个测试用例,你可以看到列表是否滚动顺畅:

// User list scroll to bottom
@Test
fun getScrollToBottom(){
    onView(withId(R.id.userLists))
        .perform(scrollToPosition<RecyclerView.ViewHolder>(activityTestRule.activity.userLists.adapter!!.itemCount - 1))
}

Espresso 还有很多其他功能。你可以查看这个作弊表(developer.android.com/training/testing/espresso/cheat-sheet),这是由 Google 提供的。

摘要

测试是发现 UI、后端代码或逻辑中错误的好方法。它有助于理解崩溃的原因。在这里,我们学习了两个强大的框架。一个是 JUnit,另一个是 Espresso。在本章中,我们看到了如何添加测试的依赖项。我们学习了如何将 JUnit 实现到项目中。我们看到了如何在测试用例中使用逻辑,以及如何检查测试用例的通过或失败结果。此外,我们还看到了如何连接数据库进行测试。然后,我们看到了如何将演示对象插入数据库,并随后从数据库中检索它,之后我们进行了对象匹配。

在 Android 测试中,我们使用了 Espresso 框架来测试 UI。最后,我们看到了一些 Espresso API 的用法以及如何处理和将它们与特定的活动连接。本章为你提供了一个关于测试的简要概述,以便你可以使用测试用例来完善你的项目。如果你想要了解更多,请参阅 进一步阅读 部分下的参考书籍。

如果你正在阅读这个段落,这意味着你已经完成了这本书,并准备好独立构建基于服务器和客户端的项目。现在你是一支孤胆英雄,能够创建具有安全性、数据库和测试的服务器和移动应用程序。我希望你喜欢阅读这本书,并且它将成为你即将到来的项目的参考点。

问题

  1. JUnit 支持哪些类型的代码?

  2. 谁设计了 Espresso?

  3. JUnit 在哪个平台上使用?

  4. 为什么在 Android 应用中使用 Espresso?

  5. Android 测试策略是什么?

  6. 测试的标准比例是多少?

  7. 你如何在设备上测试不同的屏幕尺寸?

进一步阅读

第十一章:评估

第一章

  1. Spring 基于 Java 标准版SE)构建,但可以运行 Java 企业版EE)。

  2. 几乎所有支持 Spring 的 Java 支持的 IDE,例如 NetBeans 和 Visual Studio。

  3. Tomcat 是一个 Web 服务器。

  4. 你也可以使用 Jetty 和 Undertow 进行 Spring 开发。

  5. 不,你可以使用 IntelliJ IDEA、Visual Studio—Xamarin、PhoneGap、Corona 和 CppDroid,但强烈推荐使用 Android Studio,它是开发 Android 应用的官方 IDE。

第二章

  1. Kotlin 是一种静态类型编程语言,它编译成与 Java 相同的字节码。

  2. Kotlin 支持面向对象编程的所有特性。

  3. Kotlin 支持许多函数式编程的特性。

  4. 要定义一个只读变量,我们必须使用val关键字和var关键字来表示可变性。

  5. 要定义一个函数,我们必须使用fun关键字。函数可以被定义为第一类公民、类成员或局部变量。

第三章

  1. Spring 作为最广泛使用的 Java EE 框架而脱颖而出。Spring 框架的核心思想是 依赖注入面向切面编程。Spring 框架也可以用于普通的 Java 应用程序,通过实现依赖注入,实现各个部分的解耦,并且我们可以执行跨切面任务。

  2. 依赖注入配置设计使我们能够移除硬编码的依赖,使我们的应用程序几乎解耦,可扩展和可行。我们可以执行一个依赖注入示例,将依赖目标从编译时移动到运行时。使用依赖注入的一些优点包括关注点分离、减少样板代码、可配置部分和简单的单元测试。

  3. 面向切面编程AOP)是一种编程范式,它通过将软件应用程序的关注点隔离来补充 面向对象编程OOP),以增强模块化。

  4. 控制反转IoC)是用于实现对象依赖之间解耦的工具。为了实现解耦和运行时对象的动态绑定,对象定义了由其他构建代理对象注入的依赖项。Spring IoC 容器是注入依赖到对象并为其准备的程序。

  5. 任何由 Spring IoC 容器引入的普通 Java 类都称为 Spring Bean。我们使用 Spring ApplicationContext 来获取 Spring Bean 实例。

  6. 与 MVC 配置设计类似,控制器是处理所有客户请求并将它们发送到安排的资源以处理的类。在 Spring MVC 中,org.springframework.web.servlet.DispatcherServlet 是前端控制器类,它根据 Spring Bean 配置引入上下文。

  7. DispatcherServlet是 Spring MVC 应用程序中的前端控制器,它堆叠 Spring Bean 配置文件,声明所有已配置的 Bean。如果启用了注解,它还会过滤组件并设计任何带有@Component@Controller@Repository@Service注解的 Bean。

  8. ContextLoaderListener是启动和关闭 Spring 的根WebApplicationContext的监听器。其重要角色是将ApplicationContext的生命周期绑定到ServletContext的生命周期,并自动生成ApplicationContext。我们可以使用它来定义可以在多个 Spring 上下文中使用的共享 Bean。

  9. 模板代码是重复出现的代码,用于类似的目的。

第四章

  1. REST 代表表示性状态转移;它是构建网络编程接口的一个新概念。

    RESTFUL 指的是通过应用 REST 构建思想组成的网络服务,被称为 RESTful 服务。它围绕系统资产以及资产状态应该如何通过 HTTP 协议传输到各种不同语言的客户端展开。在 RESTful 网络服务中,可以使用诸如GETPOSTPUTDELETE等 HTTP 方法来执行 CRUD 操作。

  2. 创建 Web API 的架构风格如下:

    • HTTP 用于客户端-服务器通信

    • XML/JSON 作为格式化语言

    • 简单 URI 作为服务的地址

    • 无状态通信

  3. SOAPUI工具用于SOAP WS和 Firefox 海报插件用于RESTFUL服务。

  4. 基于 REST 架构的 Web 服务被称为 RESTful Web 服务。这些服务使用 HTTP 方法来执行 REST 架构的理念。RESTful Web 服务通常定义一个 URI(统一资源标识符),一个服务,提供资产描述,例如 JSON 和一组 HTTP 方法。

  5. URI 代表统一资源标识符。REST 架构中的每个资源都通过其 URI 来识别。URI 的目的是在提供 Web 服务的服务器上找到资源。

  6. 它是一个 HTTP 成功代码:OK

  7. 它是一个 HTTP 客户端错误代码:Not Found

第五章

  1. Spring 安全主要针对两个领域,这些领域是认证和授权。

  2. SecurityContextSecurityContextHolder类是 Spring 安全的一部分。

  3. DelegatingFilterProxy类对于 Spring 安全是必需的,这个类来自package org.springframework.web.filter

  4. 是的。Spring 安全支持密码散列。

  5. 授权代码、隐式、密码、客户端凭证、设备代码、刷新令牌。

第六章

  1. H2 是一个非常轻量级的开源 Java 数据库,它可以嵌入到 Java 应用程序中。它也可以在客户端-服务器模型上运行。

  2. 资源意味着在REST架构中数据将如何表示。它允许客户端使用 HTTP 方法(例如GETPOSTPUTDELETE等)读取、写入、修改和创建资源。

  3. CRUD 代表创建、读取、更新和删除。

  4. DAO 是数据持久化的抽象。Repository是对象集合的抽象。

  5. SQLite 使用动态类型。内容可以存储为INTEGERREALTEXTBLOBNULL

  6. SQLite 数据库的替代方案有 OrmLite,Couchbase Lite 和 Snappy DB。

  7. 标准的 SQLite 命令有 SELECT, CREATE, INSERT, UPDATE, DROP, 和 DELETE

  8. SQLite 有一些缺点。如下所示:

    • 它用于处理低到中等的 HTTP 请求流量。

    • 在大多数情况下,SQLite 的大小限制为 2 GB。

第七章

  1. 调用栈是线程或协程分配的内存的一部分,包含在线程或协程上下文中调用的函数的堆栈和局部变量。

  2. 线程池是一种使用一组等待从队列中获取工作的线程的模式的抽象。

  3. 回调是一种用于传递异步操作结果的模式。

  4. 协程是轻量级线程,因为创建协程不需要像创建线程那样多的资源。

第八章

  1. 响应式编程是一种处理异步事件的途径。

  2. Mono 是一个可以发出零个或一个事件的发布者。

  3. Observable 是 RxJava 中的一个类,它发出一系列值。

  4. 调度器是线程池的抽象。

第九章

  1. EER 代表Enhanced Entity-Relationship。它是一个高级模型,是对 ER 模型修改和传递版本的模型。它有助于更精确地创建数据库模式。

  2. CRUD 代表创建、读取、更新和删除。

  3. Postgresql,MySQL,MongoDB 等等。

  4. Postman。Insomnia 易于使用且功能强大。

  5. 最小 API 为 21;目标和最大值将是最新 API(当前最新 API 是 28)。

  6. MVC, MVP 和 MVVM。

  7. Android Studio, Genymotion(免费/痛苦),Remix OS 和 Nox Player。

  8. ANR 是Application Not Responding 的缩写。当应用程序因后台的一些错误而卡在 UI 上时,就会发生这种情况。

  9. Sketch 是最好的,Adobe XD 也是设计原型的一个强大应用。

第十章

  1. 基于文本的命令行,以及基于 AWT 和 Swing 的图形测试机制。

  2. Google。

  3. 在基于 Java 的应用程序中。

  4. 单元测试、集成测试和 UI 测试。

  5. 70%小(单元测试),20%中等(集成测试),和 10%大(UI 测试)。

  6. 处理它的最佳方式是使用模拟器。

posted @ 2025-10-24 10:04  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报