1. 概述

GraphQL是一种用于Web API的查询和操作语言。为了使与GraphQL的集成更加流畅,SPQR(Schema Publisher & Query Resolver)库应运而生。

在这个教程中,我们将学习GraphQL SPQR的基本知识,并在一个简单的Spring Boot项目中看到它的应用。

2. 什么是GraphQL SPQR?

GraphQL是由Facebook创建的知名查询语言。核心是模式定义文件,其中我们定义自定义类型和函数。

传统方法是,要在项目中添加GraphQL,通常需要两步:首先,我们需要在项目中添加GraphQL模式文件;其次,需要为模式文件中的每个类型编写相应的Java持久化对象(POJO)。这意味着我们在模式文件和Java类中维护着相同的信息,这既容易出错又增加了项目的维护负担。

GraphQL Schema Publisher & Query Resolver(简称SPQR)是为了减少上述问题而诞生的,它能从注解的Java类自动生成GraphQL模式。

3. 使用Spring Boot引入GraphQL SPQR

接下来,我们将通过一个简单服务来展示SPQR的使用。我们将使用graphql-spring-boot-starter和GraphQL SPQR。

3.1. 设置

首先,我们在pom.xml中添加SPQR和Spring Boot的依赖:

<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. 创建BookService

为了管理书籍集合,我们将创建一个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暴露服务

最后一步是创建解析器,以暴露GraphQL的查询和突变。为此,我们将使用两个重要的SPQR注解:@GraphQLMutation@GraphQLQuery

@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的@RestController。为了通过SPQR暴露服务,我们需要配置GraphQLSchemaGraphQL对象:

@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端点来查看结果。例如,执行以下cURL命令获取所有Book记录:

curl -g \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"query":"{getAllBooks {id author title }}"}' \
  http://localhost:8080/graphql

3.7. 测试

配置完成后,我们可以测试我们的项目。我们将使用@SpringBootTest来测试新端点并验证响应。让我们定义JUnit测试并注入所需的WebTestClient

@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启动器,使得使用起来更加方便。让我们了解一下!

4.1. 设置

首先,在pom.xml中添加spqr-spring-boot-starter

<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添加到现有的POJO中,并配置它来处理查询和突变。然后,我们看到了在GraphQL中实际运行的新端点。最后,我们使用Spring的测试支持验证了应用程序的行为。

如往常一样,本文使用的示例代码可在GitHub上找到。SPQR Spring Boot启动套件的代码可以在GitHub上找到。