Spring Boot 与 Docker
观察 GraphQL 的实际运行

本指南将引导您创建一个应用程序,该应用程序通过基于超媒体的RESTful前端访问存储在Apache Geode中的数据。

你将构建什么

您将构建一个 Spring Web 应用程序,该应用程序允许您使用 Spring Data REST 创建和检索存储在 Apache Geode 内存数据网格 (IMDG) 中的 Person 对象。Spring Data REST 将 Spring HATEOASSpring Data for Apache Geode 的功能自动结合在一起。

Spring Data REST 还支持 Spring Data JPASpring Data MongoDBSpring Data Neo4j 作为后端数据存储,但这些内容不在本指南的范围内。

要了解更多关于 Apache Geode 概念的一般知识以及如何从 Apache Geode 访问数据,请阅读指南 使用 Apache Geode 访问数据

您需要准备的内容

如何完成本指南

与大多数 Spring 入门指南 一样,您可以从头开始并完成每个步骤,也可以跳过您已经熟悉的基本设置步骤。无论哪种方式,您最终都能获得可运行的代码。

从头开始,请继续阅读 使用 Spring Initializr 开始

跳过基础知识,请执行以下操作:

完成后,您可以将您的结果与 gs-accessing-gemfire-data-rest/complete 中的代码进行对比。

从 Spring Initializr 开始

对于所有 Spring 应用程序,您应该从 Spring Initializr 开始。Spring Initializr 提供了一种快速的方式来引入应用程序所需的所有依赖项,并为您完成大量设置工作。本示例需要 "Spring for Apache Geode" 依赖项。

以下清单展示了使用 Maven 时的 pom.xml 文件示例:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.0</version>
    </parent>

    <groupId>org.springframework</groupId>
    <artifactId>gs-accessing-gemfire-data-rest</artifactId>
    <version>0.1.0</version>

    <properties>
        <spring-shell.version>1.2.0.RELEASE</spring-shell.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-rest</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-geode</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.shell</groupId>
            <artifactId>spring-shell</artifactId>
            <version>${spring-shell.version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

下面的清单展示了使用 Gradle 时的示例 build.gradle 文件:

plugins {
    id 'org.springframework.boot' version '2.7.0'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'io.freefair.lombok' version '6.3.0'
    id 'java'
}

apply plugin: 'eclipse'
apply plugin: 'idea'

group = "org.springframework"
version = "0.1.0"
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {

    implementation "org.springframework.boot:spring-boot-starter-data-rest"
    implementation "org.springframework.data:spring-data-geode"
    implementation "org.projectlombok:lombok"

    runtimeOnly "org.springframework.shell:spring-shell:1.2.0.RELEASE"

    testImplementation "org.springframework.boot:spring-boot-starter-test"

}

test {
    useJUnitPlatform()
}

bootJar {
    baseName = 'gs-accessing-gemfire-data-rest'
    version =  '0.1.0'
}

创建领域对象

创建一个新的领域对象来表示一个人。

src/main/java/hello/Person.java

package hello;

import java.util.concurrent.atomic.AtomicLong;

import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.gemfire.mapping.annotation.Region;

import lombok.Data;

@Data
@Region("People")
public class Person {

  private static AtomicLong COUNTER = new AtomicLong(0L);

  @Id
  private Long id;

  private String firstName;
  private String lastName;

  @PersistenceConstructor
  public Person() {
    this.id = COUNTER.incrementAndGet();
  }
}

Person 拥有名字和姓氏。Apache Geode 的域对象需要一个 id,因此使用 AtomicLong 来在每次创建 Person 对象时递增。

创建 Person 仓库

接下来,您需要创建一个简单的 Repository 来持久化/访问存储在 Apache Geode 中的 Person 对象。

src/main/java/hello/PersonRepository.java

package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

@RepositoryRestResource(collectionResourceRel = "people", path = "people")
public interface PersonRepository extends CrudRepository<Person, Long> {

  List<Person> findByLastName(@Param("name") String name);

}

这个 Repository 是一个接口,将允许您执行涉及 Person 对象的各种数据访问操作(例如基本的 CRUD 和简单查询)。它通过扩展 CrudRepository 来获取这些操作。

在运行时,Spring Data for Apache Geode 会自动创建该接口的实现。然后,Spring Data REST 将使用 @RepositoryRestResource 注解来引导 Spring MVC 在 /people 路径下创建 RESTful 端点。

@RepositoryRestResource 并不是导出 Repository 所必需的。它仅用于更改导出的详细信息,例如使用 /people 而不是默认的 /persons

在这里,您还定义了一个自定义查询,用于根据 lastName 检索 Person 对象列表。您将在本指南的后面部分了解如何调用它。

使应用程序可执行

尽管可以将此服务打包为传统的WAR文件以部署到外部应用服务器,但下面展示的更简单的方法是创建一个独立的应用程序。您将所有内容打包到一个可执行的JAR文件中,由一个经典的Java main()方法驱动。在此过程中,您使用Spring对嵌入Tomcat servlet容器的支持作为HTTP运行时,而不是部署到外部的servlet容器。

src/main/java/hello/Application.java

package hello;

import org.apache.geode.cache.client.ClientRegionShortcut;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.gemfire.config.annotation.ClientCacheApplication;
import org.springframework.data.gemfire.config.annotation.EnableEntityDefinedRegions;
import org.springframework.data.gemfire.repository.config.EnableGemfireRepositories;

@SpringBootApplication
@ClientCacheApplication(name = "AccessingGemFireDataRestApplication")
@EnableEntityDefinedRegions(
  basePackageClasses = Person.class,
  clientRegionShortcut = ClientRegionShortcut.LOCAL
)
@EnableGemfireRepositories
@SuppressWarnings("unused")
public class Application {

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

@SpringBootApplication 是一个便捷的注解,它添加了以下所有内容:

  • @Configuration: 将类标记为应用程序上下文的 bean 定义源。

  • @EnableAutoConfiguration: 告诉 Spring Boot 根据类路径设置、其他 bean 和各种属性设置开始添加 bean。例如,如果 spring-webmvc 在类路径上,此注解会将应用程序标记为 Web 应用程序,并激活关键行为,例如设置 DispatcherServlet

  • @ComponentScan: 告诉 Spring 在 hello 包中查找其他组件、配置和服务,使其能够找到控制器。

main() 方法使用 Spring Boot 的 SpringApplication.run() 方法来启动应用程序。您是否注意到没有一行 XML 代码?也没有 web.xml 文件。这个 Web 应用程序是 100% 纯 Java 的,您无需处理任何配置管道或基础设施。

@EnableGemfireRepositories 注解激活了 Spring Data for Apache GeodeRepositoriesSpring Data for Apache Geode 将会创建 PersonRepository 接口的具体实现,并将其配置为与 Apache Geode 的嵌入式实例进行通信。

构建可执行的 JAR {#_build_an_executable_jar}

您可以使用 Gradle 或 Maven 从命令行运行应用程序。您还可以构建一个包含所有必要依赖项、类和资源的可执行 JAR 文件,并运行该文件。构建可执行 JAR 文件使得在整个开发生命周期、跨不同环境等场景中,更方便地打包、版本控制和部署该服务。

如果您使用 Gradle,可以通过 ./gradlew bootRun 来运行应用程序。或者,您可以使用 ./gradlew build 构建 JAR 文件,然后按如下方式运行该 JAR 文件:

java -jar build/libs/gs-accessing-gemfire-data-rest-0.1.0.jar

如果您使用 Maven,可以通过 ./mvnw spring-boot:run 命令来运行应用程序。另外,您也可以使用 ./mvnw clean package 构建 JAR 文件,然后按如下方式运行该 JAR 文件:

java -jar target/gs-accessing-gemfire-data-rest-0.1.0.jar

这里描述的步骤可以创建一个可运行的 JAR 文件。您也可以 构建一个经典的 WAR 文件

日志输出已显示。服务应该会在几秒钟内启动并运行。

测试应用程序

现在应用程序已经运行,您可以对其进行测试。您可以使用任何您喜欢的 REST 客户端。以下示例使用了 *nix 工具 curl

首先,您需要查看顶级服务。

$ curl http://localhost:8080
{
  "_links" : {
    "people" : {
      "href" : "http://localhost:8080/people"
    }
  }
}

在这里,您将首次了解该服务器提供的功能。有一个位于 http://localhost:8080/peoplepeople 链接。Spring Data for Apache Geode 不像其他 Spring Data REST 指南那样支持分页,因此没有额外的导航链接。

Spring Data REST 使用 HAL 格式 作为 JSON 输出。它非常灵活,并提供了一种便捷的方式来在提供的数据旁边附加链接。

$ curl http://localhost:8080/people
{
  "_links" : {
    "search" : {
      "href" : "http://localhost:8080/people/search"
    }
  }
}

是时候创建一个新的 Person 了!

$ curl -i -X POST -H "Content-Type:application/json" -d '{  "firstName" : "Frodo",  "lastName" : "Baggins" }' http://localhost:8080/people
HTTP/1.1 201 Created
Server: Apache-Coyote/1.1
Location: http://localhost:8080/people/1
Content-Length: 0
Date: Wed, 05 Mar 2014 20:16:11 GMT
  • -i 确保您可以看到包括头信息在内的响应消息。新创建的 Person 的 URI 将会显示出来

  • -X POST 发起一个 POST HTTP 请求以创建一个新条目

  • -H "Content-Type:application/json" 设置内容类型,以便应用程序知道负载包含一个 JSON 对象

  • -d '{ "firstName" : "Frodo", "lastName" : "Baggins" }' 是发送的数据

注意之前的 POST 操作包含了一个 Location 头信息。该信息包含了新创建资源的 URI。Spring Data REST 还在 RepositoryRestConfiguration.setReturnBodyOnCreate(…)setReturnBodyOnCreate(…) 方法中提供了两个选项,您可以使用它们来配置框架,使其立即返回刚刚创建的资源表示。

通过此操作,您可以查询所有人:

$ curl http://localhost:8080/people
{
  "_links" : {
    "search" : {
      "href" : "http://localhost:8080/people/search"
    }
  },
  "_embedded" : {
    "persons" : [ {
      "firstName" : "Frodo",
      "lastName" : "Baggins",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/people/1"
        }
      }
    } ]
  }
}

people 集合资源包含一个包含 Frodo 的列表。请注意它如何包含一个 self 链接。Spring Data REST 还使用 Evo Inflector 来对实体名称进行复数化以进行分组。

您可以直接查询单个记录:

$ curl http://localhost:8080/people/1
{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/people/1"
    }
  }
}

这看起来可能完全是基于网页的,但在幕后,它实际上是在与一个嵌入式 Apache Geode 数据库进行通信。

在本指南中,只有一个领域对象。在一个更复杂的系统中,领域对象之间相互关联,Spring Data REST 将渲染额外的链接以帮助导航到相关的记录。

查找所有自定义查询:

$ curl http://localhost:8080/people/search
{
  "_links" : {
    "findByLastName" : {
      "href" : "http://localhost:8080/people/search/findByLastName{?name}",
      "templated" : true
    }
  }
}

您可以看到包含 HTTP 查询参数 name 的查询 URL。如果您注意的话,这与嵌入在接口中的 @Param("name") 注解是匹配的。

要使用 findByLastName 查询,请执行以下操作:

$ curl http://localhost:8080/people/search/findByLastName?name=Baggins
{
  "_embedded" : {
    "persons" : [ {
      "firstName" : "Frodo",
      "lastName" : "Baggins",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/people/1"
        }
      }
    } ]
  }
}

因为您在代码中定义了返回类型为 List<Person>,所以它会返回所有结果。如果您将其定义为仅返回 Person,它会选择其中一个 Person 对象返回。由于这种行为是不可预测的,因此对于可能返回多个条目的查询,您可能不希望这样做。

您还可以发出 PUTPATCHDELETE REST 调用来替换、更新或删除现有记录。

$ curl -X PUT -H "Content-Type:application/json" -d '{ "firstName": "Bilbo", "lastName": "Baggins" }' http://localhost:8080/people/1
$ curl http://localhost:8080/people/1
{
  "firstName" : "Bilbo",
  "lastName" : "Baggins",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/people/1"
    }
  }
}
$ curl -X PATCH -H "Content-Type:application/json" -d '{ "firstName": "Bilbo Jr." }' http://localhost:8080/people/1
$ curl http://localhost:8080/people/1
{
  "firstName" : "Bilbo Jr.",
  "lastName" : "Baggins",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/people/1"
    }
  }
}

PUT 会替换整个记录。未提供的字段将被替换为 nullPATCH 可用于更新部分项目。

您可以删除记录:

$ curl -X DELETE http://localhost:8080/people/1
$ curl http://localhost:8080/people
{
  "_links" : {
    "search" : {
      "href" : "http://localhost:8080/people/search"
    }
  }
}

这种超媒体驱动的接口的一个非常便利之处在于,您可以使用 curl(或任何您使用的 REST 客户端)发现所有的 RESTful 端点。您无需与客户交换正式的合同或接口文档。

总结

恭喜!您刚刚开发了一个基于超媒体RESTful前端和基于Apache Geode后端的应用程序。

另请参阅

以下指南也可能有所帮助:

本页目录