1. 引言
GraphQL 是一種用於 Web API 查詢和操作的語言。 用於使與 GraphQL 交互更加無縫的庫之一是 SPQR。
在本教程中,我們將學習 GraphQL SPQR 的基本知識,並在一個簡單的 Spring Boot 項目中看到它的實際應用。
2. GraphQL SPQR 是什麼?
GraphQL 是一種由 Facebook 創建的知名查詢語言。其核心是模式——在其中我們定義自定義類型和函數的“文件”。
在傳統的做法中,如果我們想將 GraphQL 添加到我們的項目,我們需要遵循兩步。第一步,我們需要將 GraphQL 模式文件添加到項目中。第二步,我們需要編寫相應的 Java POJO 類,以表示模式中的每個類型。 這意味着我們將在模式文件和 Java 類中維護相同的信息。 這種方法容易出錯,並且需要更多的工作來維護項目。
GraphQL Schema Publisher & Query Resolver,簡稱 SPQR,起源於為了解決上述問題——它只需從註釋的 Java 類生成 GraphQL 模式。
3. 介紹 GraphQL SPQR 與 Spring Boot
為了親身體驗 SPQR 的強大功能,我們將搭建一個簡單的服務。我們將使用 GraphQL Spring Boot Starter 以及 GraphQL SPQR。
3.1. 環境搭建
讓我們首先將 SPQR 和 Spring Boot 的依賴項添加到我們的 POM 中:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
<dependency>
<groupId>io.leangen.graphql</groupId>
<artifactId>spqr</artifactId>
<version>0.12.4</version>
</dependency>3.2. 編寫 Book 類
現在我們已經添加了必要的依賴項,讓我們創建一個簡單的 Book 類:
public class Book {
private Integer id;
private String author;
private String title;
}正如上面所見,它不包含任何 SPQR 註解。 這在我們不擁有源代碼的情況下,但希望從該庫中獲益時非常有用。
3.3. 書籍服務編寫
為了管理書籍集合,我們創建一個 IBookService 接口:
public interface IBookService {
Book getBookWithTitle(String title);
List<Book> getAllBooks();
Book addBook(Book book);
Book updateBook(Book book);
boolean deleteBook(Book book);
}
然後,我們將提供我們接口的實現:
@Service
public class BookService implements IBookService {
private static final Set<Book> BOOKS_DATA = initializeData();
@Override
public Book getBookWithTitle(String title) {
return BOOKS_DATA.stream()
.filter(book -> book.getTitle().equals(title))
.findFirst()
.orElse(null);
}
@Override
public List<Book> getAllBooks() {
return new ArrayList<>(BOOKS_DATA);
}
@Override
public Book addBook(Book book) {
BOOKS_DATA.add(book);
return book;
}
@Override
public Book updateBook(Book book) {
BOOKS_DATA.removeIf(b -> Objects.equals(b.getId(), book.getId()));
BOOKS_DATA.add(book);
return book;
}
@Override
public boolean deleteBook(Book book) {
return BOOKS_DATA.remove(book);
}
private static Set<Book> initializeData() {
Book book = new Book(1, "J. R. R. Tolkien", "The Lord of the Rings");
Set<Book> books = new HashSet<>();
books.add(book);
return books;
}
}3.4. 使用 graphql-spqr 暴露服務
剩下的只是創建解析器(resolver),它將暴露 GraphQL 的 mutation 和 query。為此,我們將使用兩個重要的 SPQR 註解:<em @GraphQLMutation</em> 和 <em @GraphQLQuery</em>。
@Service
public class BookResolver {
@Autowired
IBookService bookService;
@GraphQLQuery(name = "getBookWithTitle")
public Book getBookWithTitle(@GraphQLArgument(name = "title") String title) {
return bookService.getBookWithTitle(title);
}
@GraphQLQuery(name = "getAllBooks", description = "Get all books")
public List<Book> getAllBooks() {
return bookService.getAllBooks();
}
@GraphQLMutation(name = "addBook")
public Book addBook(@GraphQLArgument(name = "newBook") Book book) {
return bookService.addBook(book);
}
@GraphQLMutation(name = "updateBook")
public Book updateBook(@GraphQLArgument(name = "modifiedBook") Book book) {
return bookService.updateBook(book);
}
@GraphQLMutation(name = "deleteBook")
public void deleteBook(@GraphQLArgument(name = "book") Book book) {
bookService.deleteBook(book);
}
}如果不想在每個方法中都編寫 @GraphQLArgument,並且滿足 GraphQL 參數命名為輸入參數的要求,則可以使用帶有 -parameters 選項的編譯器進行編譯。
3.5. REST 控制器
最後,我們將定義一個 Spring <em @RestController</em>。為了使用 SPQR 暴露服務,我們將配置 <em GraphQLSchema</em> 和 <em GraphQL</em> 對象:
@RestController
public class GraphqlController {
private final GraphQL graphQL;
@Autowired
public GraphqlController(BookResolver bookResolver) {
GraphQLSchema schema = new GraphQLSchemaGenerator()
.withBasePackages("com.baeldung")
.withOperationsFromSingleton(bookResolver)
.generate();
this.graphQL = new GraphQL.Builder(schema)
.build();
}需要注意的是,我們必須將 BookResolver 註冊為單例。
在與 SPQR 的旅程中最後一步是創建 /graphql 端點。它將作為與我們服務的一個單一接觸點,執行請求的查詢和變異:
@PostMapping(value = "/graphql")
public Map<String, Object> execute(@RequestBody Map<String, String> request, HttpServletRequest raw)
throws GraphQLException {
ExecutionResult result = graphQL.execute(request.get("query"));
return result.getData();
}
}3.6. 結果
我們可以通過檢查 graphql</em/> 端點來驗證結果。例如,讓我們通過執行以下 cURL 命令來檢索所有 Book</em/> 記錄:
curl -g \
-X POST \
-H "Content-Type: application/json" \
-d '{"query":"{getAllBooks {id author title }}"}' \
http://localhost:8080/graphql3.7. 測試
完成配置後,我們可以對我們的項目進行測試。我們將使用 <em @SpringBootTest</em>> 來測試我們的新端點並驗證響應。 讓我們定義 JUnit 測試並注入所需的 <em WebTestClient</em>>:
@SpringBootTest(webEnvironment = RANDOM_PORT, classes = SpqrApp.class)
class SpqrAppIntegrationTest {
private static final String GRAPHQL_PATH = "/graphql";
@Autowired
private WebTestClient webTestClient;
@Test
void whenGetAllBooks_thenValidResponseReturned() {
String getAllBooksQuery = "{getAllBooks{ id title author }}";
webTestClient.post()
.uri(GRAPHQL_PATH)
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(toJSON(getAllBooksQuery)), String.class)
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.getAllBooks").isNotEmpty();
}
@Test
void whenAddBook_thenValidResponseReturned() {
String addBookMutation = "mutation { addBook(newBook: {id: 123, author: \"J. K. Rowling\", "
+ "title: \"Harry Potter and Philosopher's Stone\"}) { id author title } }";
webTestClient.post()
.uri(GRAPHQL_PATH)
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(toJSON(addBookMutation)), String.class)
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.addBook.id").isEqualTo("123")
.jsonPath("$.addBook.title").isEqualTo("Harry Potter and Philosopher's Stone")
.jsonPath("$.addBook.author").isEqualTo("J. K. Rowling");
}
private static String toJSON(String query) {
try {
return new JSONObject().put("query", query).toString();
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
}4. 使用 GraphQL SPQR Spring Boot Starter
團隊正在開發 SPQR 時,創建了一個 Spring Boot Starter,這使得使用它更加簡單。 讓我們來了解一下!
4.1. 部署準備
我們將首先添加 spqr-spring-boot-starter 到我們的 POM 文件中:
<dependency>
<groupId>io.leangen.graphql</groupId>
<artifactId>graphql-spqr-spring-boot-starter</artifactId>
<version>1.0.1</version>
</dependency>4.2. BookService
然後,我們需要對我們的 BookService 進行兩個修改。首先,它必須使用 @GraphQLApi 註解進行標註。此外,我們希望在 API 中暴露的所有方法都必須具有相應的註解。
@Service
@GraphQLApi
public class BookService implements IBookService {
private static final Set<Book> BOOKS_DATA = initializeData();
@GraphQLQuery(name = "getBookWithTitle")
public Book getBookWithTitle(@GraphQLArgument(name = "title") String title) {
return BOOKS_DATA.stream()
.filter(book -> book.getTitle().equals(title))
.findFirst()
.orElse(null);
}
@GraphQLQuery(name = "getAllBooks", description = "Get all books")
public List<Book> getAllBooks() {
return new ArrayList<>(BOOKS_DATA);
}
@GraphQLMutation(name = "addBook")
public Book addBook(@GraphQLArgument(name = "newBook") Book book) {
BOOKS_DATA.add(book);
return book;
}
@GraphQLMutation(name = "updateBook")
public Book updateBook(@GraphQLArgument(name = "modifiedBook") Book book) {
BOOKS_DATA.removeIf(b -> Objects.equals(b.getId(), book.getId()));
BOOKS_DATA.add(book);
return book;
}
@GraphQLMutation(name = "deleteBook")
public boolean deleteBook(@GraphQLArgument(name = "book") Book book) {
return BOOKS_DATA.remove(book);
}
private static Set<Book> initializeData() {
Book book = new Book(1, "J. R. R. Tolkein", "The Lord of the Rings");
Set<Book> books = new HashSet<>();
books.add(book);
return books;
}
}如我們所見,我們基本上將代碼從 BookResolver 移動到了 BookService。 此外,我們不需要 GraphqlController 類——一個 /graphql 端點將會自動添加。
5. 總結
GraphQL 是一種令人興奮的框架,是傳統 RESTful 端點的替代方案。雖然它提供了很大的靈活性,但也可能帶來一些繁瑣的任務,例如維護模式文件。 SPQR 旨在使使用 GraphQL 變得更容易,並且減少出錯的可能性。
在本文中,我們學習瞭如何將 SPQR 添加到現有的 POJOs 中,並將其配置為提供查詢和 Mutation。然後,我們觀察了新的端點在 GraphQL 中的運行情況。最後,我們使用 Spring 的測試支持驗證了應用程序的行為。