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

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第四章: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 项目窗口:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/7bcaa137-45c0-45c7-bc1b-d99aec15152a.png

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

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

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/6edfddbb-1421-48a1-b5b1-bf25f815c0c2.png

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 的输出:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/2e4a3efe-9a90-438e-a6ff-3ebbcf9017b4.png

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

因此,根据 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)消息。

移动应用

所以,在一切完成后,运行你的服务器。然后,运行你的应用。以下截图显示了我们的应用输出:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/8ffb7df1-9c5d-4f8a-9556-88a7c0c99056.png

你可以随意修改,但要注意端点和模型。

摘要

在本章中,我们简要介绍了驱动 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层次结构的图示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/b5fd0389-0cd9-48a8-a03f-6954119392f0.png

根据这个图示,在一个应用中,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/并修改给定的字段以满足你的需求。你可以在以下屏幕截图中查看我们的项目信息:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/4047a3e3-1931-49b8-82b8-4674ba208e7a.png

在这里,我们使用Maven Project,选择语言为Kotlin,Spring Boot 版本为2.1.1 (SNAPSHOT)

我们已添加了SecurityWebDevTools依赖项。你可以在pom.xml中查看列表。

  1. 当你选择“生成项目”时,你会以 ZIP 文件的形式获得项目。解压并使用你的 IDE 打开此项目。

  2. 下载和更新 Maven 依赖项需要一点时间。以下是你的项目内容的截图:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/0901227f-9f7f-40af-930f-25108808826a.png

如果你需要添加新的依赖项或更新版本,请修改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错误,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/d117a43c-a6bc-4e69-ab94-3b5758114780.png

虽然你使用的是基本认证,但你必须输入用户名密码才能完成此请求。你需要点击 Auth(第二个标签)并选择Basic认证;你可以在那里输入用户名密码。如果你输入随机的用户名和密码,你也会得到相同的错误。

在输入正确的用户名密码后,你将得到以 JSON 格式输出的用户列表,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/7f32d0f3-0f92-4f2a-82af-f4b5e82cc79e.png

你也可以在浏览器中测试。在那里,你会被要求输入用户名密码

你也可以使用浏览器查看 REST API:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/4baeb0f8-550c-40e2-ba42-9dd827b76de6.png

在输入用户名和密码后,我们可以看到用户列表:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/944f295e-d503-4c56-a601-0776e8922b25.png

你已经使用 Spring Security 基本认证创建了一个非常简单的项目。我们希望从现在开始,你可以借助 Spring Security 编写自己的基于认证的项目。

创建 Android 应用程序

是时候创建一个简单的 Android 应用程序作为客户端,从我们的基本认证服务器检索 REST API 了。首先,我们需要在 Android Studio 中创建一个应用程序并填写你的项目名称和公司域名。别忘了勾选Include Kotlin support。以下是创建应用程序项目窗口的截图:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/aabf30ec-8bc8-41b7-ae73-8591beadad9f.png

从手机和平板选项中选择最低 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 的输出,我们可以看到五个用户的详细信息:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/26a0fb10-ac11-46cd-b71f-a3f08f20d12e.png

根据 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 项目,然后运行你的应用程序。以下是应用程序的输出,我们可以看到用户详情:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/bb1d3a80-1f6b-4d39-9de8-b3a746152d48.png

在下面的屏幕截图中,左侧是服务器 API,其中包含用户详情,右侧是 Android 应用程序的客户端输出:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/2deb9410-34b7-440c-9604-1e9e61a024d0.png

我们创建了一个客户端应用程序,它将获取基于基本认证的 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 协议的一般工作流程图(每个协议的流程并不固定;它基于授权的类型):

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/3a2aa391-2ab8-4090-bee9-6ed59fd69490.png

下面是工作流程的步骤:

  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

检查输出

如果你正在阅读本节,这意味着你已经正确配置了一切。完成项目后,你将拥有以下文件:

完成设置后,运行项目。如果没有错误,你可以找到运行窗口。以下截图显示没有错误,应用程序已准备好使用:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/6e2f5ec2-e29c-4641-8b42-0288911faa18.png

检查未受保护的 URL

现在,打开 Insomnia 应用程序。

从顶部邮箱创建一个 GET 请求,并使用 http://localhost:8081/open_for_all 的 URL。

你的结果将类似于以下截图:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/956bc690-72ea-4a2e-b902-eec4e709ee8a.png

ResourceServerConfig类中,我们配置了"/open_for_all"可以被每个人访问。

获取访问令牌

从顶部邮箱创建一个POST请求,并写下http://localhost:8081/oauth/token URL。这是获取令牌的默认POST URL

在多部分窗口中添加三个参数—username=sunnatpassword=passwordgrant_type=password—:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/b79568f5-19bf-4d32-b9c1-7f582311a7f4.png

你可以在SecurityConfiguration类中找到usernamepassword的信息,而grant_type可以在AuthorizationServerConfig中找到。在这里,我们使用密码授权类型。

前往基本窗口并输入用户名密码。你可以在AuthorizationServerConfig类中找到这些信息,其中用户名在withClient()中提及,密码在secret()中。

我们添加了一张图片,展示了我们记录了用户名密码的 Insomnia 工具。现在点击发送按钮。如果没有错误,你将获得以下access_token

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/2fb2b231-90a0-4d9e-af22-e2249b6e8482.png

你可以看到将用于访问受保护资源的access_token。"expires_in"表示在4469秒后令牌将过期。"scope": "read write trust"表示你可以读取、写入和修改资源。

访问受保护的 URL

我们找到了access_token,现在我们将使用它。为此,创建另一个GET请求并插入http://localhost:8081/private

作为参数,使用具有给定令牌键值的access_token,然后点击发送:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/a4b3e161-225e-4b04-bef5-76bdb512aa5f.png

完成这些操作后,你可以访问受保护的/private URL,该 URL 在ResourceServerConfig类中进行了配置。

我们现在已准备好在我们的项目中使用 OAuth2 Spring Security。

常见错误和错误

在这个项目中,你可能会遇到一些常见错误。

例如,在构建和运行项目时可能会遇到一些错误。为了解决这个问题,请检查所有依赖项的版本是否为最新。此外,请确保每个依赖项都已存在。如果你使用数据库,请确保在application.properties中你有正确的数据库和模式名称。

POST请求中,有时你会找到以下错误信息:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/d1d5e9c8-90b9-4bae-b247-5bf90ca2d651.png

之前的截图表明你输入了错误的grant_type。请检查参数以及你提及grant_typeAuthorizationServerConfig类:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/e36835a3-80c0-47dc-a8dc-c52f7499284b.png

请检查SecurityConfiguration类,并将系统的username-password与提供的usernamepassword参数匹配。以下截图表示你在Basic Auth选项卡中输入了错误的客户端或密钥值:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/6ed25197-c442-4fb8-ae9b-3cdc26f076d0.png

上述截图表示你在基本认证选项卡中输入了错误的 clientsecret 值。请将 AuthorizationServerConfig 中的 clientsecret 值与基本认证选项卡中的值进行匹配:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/ac63e400-14b3-44c0-92ab-5334b51000df.png

上述截图表示您的令牌密钥已过期。您需要刷新一个新的访问令牌来解决此错误。

你可能会遇到其他错误。要查看解决方案,你总是可以搜索 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 的图示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/d25e9f1c-fb8a-4046-8422-fa9d3ca02d48.png

数据库管理系统(DBMS)有三个重要特性,这些特性包括数据、数据库引擎和数据库模式。数据是一系列信息的集合,数据库引擎允许数据被锁定、访问和修改,而数据库模式定义了数据库的逻辑结构。

DBMS 提供了一个通用的视图,说明多个用户可以从多个位置以受控的方式访问数据。它还限制了用户对用户数据的访问。数据库模式提供了用户如何查看数据的逻辑。DBMS 处理所有请求并在数据库上执行它们。

DBMS 提供了逻辑和物理数据独立性。这意味着应用程序可以使用 API 来利用数据库中的数据。此外,客户端和应用程序无需担心存储数据的地点以及数据物理结构的更改,如存储和硬件。

流行数据库模型及其管理系统包括以下内容:

  • 关系数据库管理系统(RDBMS

  • NoSQL DBMS

  • 内存数据库管理系统(IMDBMS

  • 列式数据库管理系统(CDBMS

  • 基于云的数据管理系统

Spring 中的数据访问

数据访问负责授权访问数据存储库。它有助于区分 角色 能力,如应用程序中的用户或管理员。它维护数据访问系统,如基于角色的插入、检索、更新或删除。在 第三章,Spring 框架概述 中,我们学习了 Spring 的架构。

下面是 Spring 架构的图示,其中 数据访问 是其中一层:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/76a06808-b5d5-4223-957f-a4c24fa9088a.png

如您所见,数据访问 是 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 表中看到内容,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/06df5d5d-6d98-4dfd-b94a-a44ed7f00a95.png

创建模型

在这个项目中,我们将创建一个 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 标签,如下截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/5a54fe0a-7b95-4c30-9c1c-1d0e2a632881.png

现在,打开 Insomnia 应用程序。让我们在这个应用程序中应用 REST API 请求。

获取用户列表

使用此GET请求与此 URL:http://localhost:8080/users,然后点击发送。用户详细信息将从数据库中检索,您可以看到返回的 JSON 值,如下截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/71c87cb5-fd73-4546-89b8-355d4bb07264.png

通过 ID 获取一个用户

使用此 URL:http://localhost:8080/user/1创建一个GET函数并点击发送。用户详细信息将从数据库中检索,您可以看到具有id1的用户返回的 JSON 值,如下截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/636d9fb9-4c4d-4cdd-9943-d39d4ac5596a.png

插入新用户

使用此 URL:http://localhost:8080/user/new创建一个POST函数并点击发送。这将向数据库中插入一个用户并显示新的用户详细信息,如下截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/3373d7b0-1c14-4c8b-801b-d4c4e6a811e4.png

如果您使用/usersGET请求 URL 路径,您可以检查包含新用户的用户列表:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/998d9d15-db95-4257-9342-9b940defbb2e.png

更新用户

使用此 URL:http://localhost:8080/user/8创建一个UPDATE函数并点击发送。它将更新数据库中具有编号八的用户,并显示更新的用户信息,如下截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/207e2708-513c-4c16-ac8b-a8be932f2d29.png

如果您使用http://localhost:8080/user/8GET请求 URL 路径,您可以检查具有新详细信息的新的用户,如下截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/957a9089-b535-4d62-be91-8a380315a01f.png

删除用户

使用此 URL 创建一个DELETE函数:http://localhost:8080/users,然后点击发送。这将从数据库中删除特定的用户,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/c140f020-594e-4845-80ca-b03ae30ccd9e.png

如果你检查所有用户,那么你会看到只有七个。

最后,我们已经创建了一个使用 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 的类级别架构:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/23abc5c8-a595-4dc1-9048-2070b34bd557.png

让我们描述一下这个图表:

  • 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 类中提到的表名。

这是没有表的架构截图:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/188960bb-780e-4a2d-8682-c8b01b212ced.png

让我们运行应用程序,再次检查数据库,并刷新模式。注意现在有一个表格,正如我们在UserModel@Table注解中提到的。这个表格包含该对象的所有列,包括idnameemailcontact_number

这是更新后的数据库截图:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/1218d344-9441-4430-8872-30c29dc0306b.png

测试系统与 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 组件。

这是此架构的图示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/4036109a-4261-42a4-8c4a-827082753e00.png

以下是对所有组件的简要描述:

  • UI 控制器:活动、片段等 UI 组件位于此组件下。

  • ViewModel:这个通过模型获取数据,并将其提供给 UI。

  • LiveData:这个类持有可观察的数据。这是生命周期感知的,与常规的可观察数据不同。

  • Repository:这个用于管理多个数据源。

  • Room 数据库:这是顶层数据库层,来自 SQLite 数据库。

  • Entity:这描述了一个数据库表。

  • DAO:全称是数据访问对象DAO),它映射 SQL 查询。

  • SQLite 数据库:数据使用此在设备中存储。它由 room 创建和维护。

创建 Android 应用

让我们创建一个简单的具有数据库的 Android 应用。这将存储用户的详细信息(包括姓名、联系电话和电子邮件 ID),并使用RecyclerView在列表中显示这些详细信息:

首先,我们需要使用 Android Studio 创建一个应用,写下你的项目名称和公司域名。别忘了勾选“包含 Kotlin 支持”,使其成为一个基于 Kotlin 的应用。以下截图显示了“创建 Android 项目”窗口:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/5aa17e9c-f14d-4317-8c77-eb74dd4679b4.png

现在从“手机和平板”选项中选择最低 API 版本。此项目不需要添加其他选项。在点击“添加活动到移动”中的“下一步”后,你可以选择“基本活动”,然后重命名活动名称布局,点击“完成”。在构建项目后,你将准备好开始创建 Android 应用。

这是“添加活动到移动”窗口的截图,我们选择基本活动模板,如下截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/26c14aa1-f124-4f41-9194-6b8143ecb1f0.png

此项目的最终文件如下截图所示,其中显示了完成此项目后所有文件和资源:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/60d4d013-e5f3-4f79-a3c5-23a86660368c.png

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的空活动,如下截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/a210195e-3448-4fc6-a628-916ba645cdb0.png

下面是这个名为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 设备或模拟器上运行应用后,你会看到这个屏幕:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/4e30c6d3-fb11-45ab-99ad-43a3e6d4ab73.png

我们可以在这里看到我们预先添加的用户详细信息。现在点击浮动按钮,进入新用户活动页面,在那里你可以写下用户信息,如图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/02cff585-854a-4cb1-bed2-014976dcde08.png

最后,点击保存按钮。你现在可以看到新的用户名,如图中所示为Naruto

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/lrn-spr-andr-appdev/img/a347746d-1a37-4c8a-8e10-fdd0e2378794.png

因此,这样我们就学会了如何使用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 的缺点是什么?

进一步阅读

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值