一种灵活的API设计模式:在Spring Boot中支持GraphQL
导读:GraphQL是一种基于api的查询语言,它提供了一种更高效、强大和灵活的数据提供方式。它是由Facebook开发和开源,目前由来自世界各地的大公司和个人维护。本文作者先介绍了GraphQL,随后通过示例详细说明了GraphQL的开发流程是如何使用。
你可能已经听说过GraphQL以及Facebook如何在其移动应用中使用GraphQL。 在此本文中,我将向你展示如何在Spring Boot中组合GraphQL,看看GraphQL到底提供了什么样的功能。
为什么使用GraphQL?
GraphQL是REST API的查询语言。 GraphQL不受任何特定数据库或存储引擎的约束。 你现有的技术架构通常都支持GraphQL。
GraphQL的主要优势:
与REST不同,GraphQL无需在应用程序中创建多个API(应用程序编程接口)endpoint,在REST中,我们公开了多个endpoint以检索此类数据。
https://localhost:8080/person
https://localhost:8080/person/{id}
使用GraphQL,我们可以按需获取数据。 这与REST不同,在REST实现中,即使只需要一些属性的值,我们也会获取完整的数据响应。 例如,当我们查询REST API时,即使仅需要id和name,我们也会获得如下所示的完整数据。
{“id”: “100”,”name”: “Vijay”,”age”:34"city”: “Faridabad”,”gender”: “Male”}
通过REST API可以将前端(例如移动应用程序)与GraphQL集成在一起,并且响应非常迅速。
本文将介绍如何构建一个Spring Boot应用程序来存储和书查询籍信息。
创建应用
访问Spring Initializr或使用IntelliJ IDEA Ultimate生成具有Web,HSQLDB,Spring Boot 2.1.4等依赖项的Spring Boot应用程序。
生成的POM如下。
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/>
</parent>
<artifactId>springboot.graphql.app</artifactId>
<name>springboot-graphql-app</name>
<description>Demo project for Spring Boot with Graph QL</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>3.6.0</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>3.2.0</version>
</dependency>
</dependencies>
添加EndPoint
让我们从BookController开始,如下所示。
package graphqlapp.controller;
import graphqlapp.service.GraphQLService;
import graphql.ExecutionResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
public class BookController {
private static Logger logger = LoggerFactory.getLogger(BookController.class);
private GraphQLService graphQLService;
public BookController(GraphQLService graphQLService) {
this.graphQLService=graphQLService;
}
public ResponseEntity<Object> getAllBooks( String query){
logger.info(“Entering getAllBooks@BookController”);
ExecutionResult execute = graphQLService.getGraphQL().execute(query);
return new ResponseEntity<>(execute, HttpStatus.OK);
}
}
添加model
接下来,我们将添加一个model类来代表书。 我们将其命名为Book。 model类的代码如下。
package graphqlapp.model;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
public class Book {
private String isn;
private String title;
private String publisher;
private String publishedDate;
private String[] author;
public Book() {
}
public Book(String isn, String title, String publisher, String publishedDate, String[] author) {
this.isn = isn;
this.title = title;
this.publisher = publisher;
this.publishedDate = publishedDate;
this.author = author;
}
public String getIsn() {
return isn;
}
public void setIsn(String isn) {
this.isn = isn;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getPublisher() {
return publisher;
}
public void setPublisher(String publisher) {
this.publisher = publisher;
}
public String getPublishedDate() {
return publishedDate;
}
public void setPublishedDate(String publishedDate) {
this.publishedDate = publishedDate;
}
public String[] getAuthor() {
return author;
}
public void setAuthor(String[] author) {
this.author = author;
}
}
创建BookRepository
BookRepository扩展了JpaRepository,如下所示。
package graphqlapp.repository;
import graphqlapp.model.Book;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BookRepository extends JpaRepository<Book, String> {
}
添加GraphQL模式(schema)
接下来,我们将在资源文件夹中编写一个GraphQL模式,名为books.graphql。
schema{
query:Query
}type Query{
allBooks: [Book]
book(id: String): Book
}type Book{
isn:String
title:String
publisher:String
author:[String]
publishedDate:String
}
该文件是使用GraphQL的关键。 在这里,我们定义了模式,你可以将其与查询相关联。 我们还定义了查询类型。
在此示例中,我们定义了两种类型:
当用户查询所有书籍(通过使用allBooks)时,应用程序将返回一个Book数组。
当用户通过传递ID查询特定书籍时,应用程序将返回Book对象。
添加GraphQL服务
接下来,我们需要添加GraphQL服务。 让我们将其命名为GraphQLService。
package graphqlapp.service;
import graphqlapp.model.Book;
import graphqlapp.repository.BookRepository;
import graphqlapp.service.datafetcher.AllBooksDataFetcher;
import graphqlapp.service.datafetcher.BookDataFetcher;
import graphql.GraphQL;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.util.stream.Stream;
public class GraphQLService {
private static Logger logger = LoggerFactory.getLogger(GraphQLService.class);
private BookRepository bookRepository;
private AllBooksDataFetcher allBooksDataFetcher;
private BookDataFetcher bookDataFetcher;
(“classpath:books.graphql”)
Resource resource;
private GraphQL graphQL;
public GraphQLService(BookRepository bookRepository, AllBooksDataFetcher allBooksDataFetcher,
BookDataFetcher bookDataFetcher) {
this.bookRepository=bookRepository;
this.allBooksDataFetcher=allBooksDataFetcher;
this.bookDataFetcher=bookDataFetcher;
}
private void loadSchema() throws IOException {
logger.info(“Entering loadSchema ”);
loadDataIntoHSQL();
//Get the graphql file
File file = resource.getFile();
//Parse SchemaF
TypeDefinitionRegistry typeDefinitionRegistry = new SchemaParser().parse(file);
RuntimeWiring runtimeWiring = buildRuntimeWiring();
GraphQLSchema graphQLSchema = new SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, runtimeWiring);
graphQL = GraphQL.newGraphQL(graphQLSchema).build();
}
private void loadDataIntoHSQL() {
Stream.of(
new Book(“1001”, “The C Programming Language”, “PHI Learning”, “1978”,
new String[] {
“Brian W. Kernighan (Contributor)”,
“Dennis M. Ritchie”
}),
new Book(“1002”,”Your Guide To Scrivener”, “MakeUseOf.com”, “ April 21st 2013”,
new String[] {
“Nicole Dionisio (Goodreads Author)”
}),
new Book(“1003”,”Beyond the Inbox: The Power User Guide to Gmail”, “ Kindle Edition”, “November 19th 2012”,
new String[] {
“Shay Shaked”
, “Justin Pot”
, “Angela Randall (Goodreads Author)”
}),
new Book(“1004”,”Scratch 2.0 Programming”, “Smashwords Edition”, “February 5th 2015”,
new String[] {
“Denis Golikov (Goodreads Author)”
}),
new Book(“1005”,”Pro Git”, “by Apress (first published 2009)”, “2014”,
new String[] {
“Scott Chacon”
})
).forEach(book -> {
bookRepository.save(book);
});
}
private RuntimeWiring buildRuntimeWiring() {
return RuntimeWiring.newRuntimeWiring()
.type(“Query”, typeWiring -> typeWiring
.dataFetcher(“allBooks”, allBooksDataFetcher)
.dataFetcher(“book”, bookDataFetcher))
build();
}
public GraphQL getGraphQL(){
return graphQL;
}
}
当Spring Boot应用程序运行时,Spring框架将调用@PostConstruct方法。 @PostConstruct方法中的代码会将书籍信息写入HQL数据库中。
在此服务类的buildRuntimeWiring()方法中,我们将两个数据获取程序进行运行时绑定:allBook和book。 此处定义的名称allBookand book必须与我们已经创建的GraphQL文件中定义的类型匹配。
创建数据访问层
GraphQL模式中的每种类型都有一个对应的数据提取器(data fetcher)。
我们需要为在架构中定义的allBooks和Book类型编写两个单独的数据获取器。
allBooks类型的数据获取程器是这个。
package graphqlapp.service.datafetcher;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import graphqlapp.model.Book;
import graphqlapp.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
public class AllBooksDataFetcher implements DataFetcher<List<Book>> {
private BookRepository bookRepository;
public AllBooksDataFetcher(BookRepository bookRepository) {
this.bookRepository=bookRepository;
}
public List<Book> get(DataFetchingEnvironment dataFetchingEnvironment) {
return bookRepository.findAll();
}
}
Book类型的数据获取器是这个。
package graphqlapp.service.datafetcher;
import graphql.schema.DataFetcher;
import graphqlapp.model.Book;
import graphqlapp.repository.BookRepository;
import graphql.schema.DataFetchingEnvironment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
public class BookDataFetcher implements DataFetcher<Book> {
private BookRepository bookRepository;
public BookDataFetcher(BookRepository bookRepository){
this.bookRepository = bookRepository;
}
public Book get(DataFetchingEnvironment dataFetchingEnvironment) {
String isn = dataFetchingEnvironment.getArgument(“id”);
return bookRepository.findById(isn).orElse(null);
}
}
运行应用
我在端口9002端口上运行此应用程序。 因此,我在application.properties文件中如下。
server.port=9002
这样,我们的Spring Boot GraphQL应用程序就准备好了。 让我们运行我们的Spring Boot应用程序,并使用Postman工具对其进行测试。
注意这里我们只有一个endpoinst,http://localhost:9002/rest/books
让我们使用该单个endpoint查询多个数据集。 为此,请打开Postman并在请求正文中添加以下查询。
输入1:我们要查询一本ID为1001的特定书,并且只需要书名即可。 同时,我们正在查询allBooks,并期望响应将包含is,title,author,publisher和publishedDate。
{
”1001"){ :
title
}
allBooks{
isn
title
author
publisher
publishedDate
}
}
输出1:响应如下。
{
“errors”: [],
“data”: {
“book”: {
“title”: “The C Programming Language”
},
“allBooks”: [
{
“isn”: “1001”,
“title”: “The C Programming Language”,
“author”: [
“Brian W. Kernighan (Contributor)”,
“Dennis M. Ritchie”
],
“publisher”: “PHI Learning”,
“publishedDate”: “1978”
},
{
“isn”: “1002”,
“title”: “Your Guide To Scrivener”,
“author”: [
“Nicole Dionisio (Goodreads Author)”
],
“publisher”: “MakeUseOf.com”,
“publishedDate”: “ April 21st 2013”
},
{
“isn”: “1003”,
“title”: “Beyond the Inbox: The Power User Guide to Gmail”,
“author”: [
“Shay Shaked”,
“Justin Pot”,
“Angela Randall (Goodreads Author)”
],
“publisher”: “ Kindle Edition”,
“publishedDate”: “November 19th 2012”
},
{
“isn”: “1004”,
“title”: “Scratch 2.0 Programming”,
“author”: [
“Denis Golikov (Goodreads Author)”
],
“publisher”: “Smashwords Edition”,
“publishedDate”: “February 5th 2015”
},
{
“isn”: “1005”,
“title”: “Pro Git”,
“author”: [
“Scott Chacon”
],
“publisher”: “by Apress (first published 2009)”,
“publishedDate”: “2014”
}
]
},
“extensions”: null
}
输入2:让我们再次通过ID查询特定图书的标题和作者。
{
book(id:”1001"){
title
author
}
}
输出2:输出是这个。 我们获得ID为1001的书的标题和作者。
{
“errors”: [],
“data”: {
“book”: {
“title”: “The C Programming Language”,
“author”: [
“Brian W. Kernighan (Contributor)”,
“Dennis M. Ritchie”
]
}
},
“extensions”: null
}
输入3:让我们查询所有图书的书名,作者,出版日期和出版商详细信息
{
allBooks{
isn
title
author
publisher
publishedDate
}
}
输出3:输出是这个。
{
“errors”: [],
“data”: {
“allBooks”: [
{
“isn”: “1001”,
“title”: “The C Programming Language”,
“author”: [
“Brian W. Kernighan (Contributor)”,
“Dennis M. Ritchie”
],
“publisher”: “PHI Learning”,
“publishedDate”: “1978”
},
{
“isn”: “1002”,
“title”: “Your Guide To Scrivener”,
“author”: [
“Nicole Dionisio (Goodreads Author)”
],
“publisher”: “MakeUseOf.com”,
“publishedDate”: “ April 21st 2013”
},
{
“isn”: “1003”,
“title”: “Beyond the Inbox: The Power User Guide to Gmail”,
“author”: [
“Shay Shaked”,
“Justin Pot”,
“Angela Randall (Goodreads Author)”
],
“publisher”: “ Kindle Edition”,
“publishedDate”: “November 19th 2012”
},
{
“isn”: “1004”,
“title”: “Scratch 2.0 Programming”,
“author”: [
“Denis Golikov (Goodreads Author)”
],
“publisher”: “Smashwords Edition”,
“publishedDate”: “February 5th 2015”
},
{
“isn”: “1005”,
“title”: “Pro Git”,
“author”: [
“Scott Chacon”
],
“publisher”: “by Apress (first published 2009)”,
“publishedDate”: “2014”
}
]
},
“extensions”: null
}
参考阅读:
高可用架构
改变互联网的构建方式